TUTORIAL
Getting started
Chartado turns lines of text into an animated architecture diagram. You write services and arrows; the parser lays them out in columns and rows, and a message travels the arrows on screen. There is nothing to install and no account needed to try it.
The fastest path to a moving diagram is the live editor embedded on the AWS diagram page. It requires no login and it does not open blank: it loads with a prompt already in the box and the animation already running, which is why the playback button reads Pause rather than Play. Select the text, replace it with your own, and the diagram redraws.
Four steps below take you from one arrow to a branching, multi-line diagram. Read them once, in order; everything after this section is reference material you can dip into.
Step 1. One arrow
A -> B means A sends to B. Chain arrows to make a pipeline. The parser splits each line on ->, trims the whitespace around every segment, and assigns column 0, 1, 2 to the segments in order. That is all a linear flow is.
Your first diagram
API Gateway -> Lambda -> DynamoDB
Three services in a row at columns 0, 1 and 2, wired targets: [1], [2], []. One flow, rooted at the first service.
What the parser produces
3 services: API Gateway (column 0), Lambda (1), DynamoDB (2).
Step 2. Use your own names
Names are free text and you are not picking from a menu. The words inside a name decide which icon and colour the box gets: the parser lowercases the name and searches it for the first matching keyword. If nothing matches you get the neutral generic box, and your name still renders exactly as typed. See names and icons for the full keyword list.
Name things whatever you call them
Order API -> Checkout Function -> Orders Table
The same three-box shape in your own vocabulary. Names render exactly as typed. The icons still resolve to API Gateway, Lambda and DynamoDB because the parser found the keywords api, function and table inside them.
What the parser produces
3 services: Order API (column 0), Checkout Function (1), Orders Table (2).
Step 3. Branch
Two bracket styles do the branching. {A, B} is a fan-out: the message goes to every entry at once. |A, B| is a choice: the message takes one path. Both put their entries in the same column and give them a row and a totalRows so the layout can stack them vertically. The difference is what the animation does, and the parser records it as isFanOutSource versus isChoiceSource on the service in front of the bracket.
Fan-out: send to everything at once
API Gateway -> Lambda -> SNS -> {Lambda Emailer, SQS Archive}
Braces make SNS a fan-out source: it gets targets: [3, 4] and isFanOutSource: true. Both entries land in column 3 with rows 0 and 1 and totalRows: 2, so they stack. The animation sends a message down both arrows at the same time.
What the parser produces
5 services: API Gateway (0), Lambda (1), SNS (2), Lambda Emailer (3), SQS Archive (3).
Choice: take one path, not all of them
API Gateway -> Lambda -> |DynamoDB, SQS| -> EventBridge
Pipes make Lambda a choice source: isChoiceSource: true, targets: [2, 3]. DynamoDB and SQS are flagged isChoiceTarget and share column 2. The segment after the closing pipe rejoins, so both branches target EventBridge. Same shape as the fan-out above, different meaning at playback.
What the parser produces
5 services: API Gateway (0), Lambda (1), DynamoDB (2), SQS (2), EventBridge (3).
Step 4. Add a second line
A second line usually adds to the same picture — it only draws a separate one if it shares no service name with what you have already written. Repeat a name that already exists and the parser reuses that service instead of creating a duplicate. Write the same chain backwards on line two and you get a second flow over the same boxes: request out and response back.
A response coming back
API Gateway -> Lambda -> DynamoDB
API Gateway <- Lambda <- DynamoDB
Line two names the same three services, so no new boxes are created — the result is still 3. Because the second line runs against the direction of the first, it becomes a second flow: flows[0] carries the outbound connections and flows[1] the return. You get a request animation and a response animation over one set of boxes.
What the parser produces
3 services: API Gateway (0), Lambda (1), DynamoDB (2). Two flows.
What the public editor already shows you
API Gateway -> Lambda -> SNS
SNS -> {Lambda Emailer, Lambda Analytics, SQS Archive}
This is the prompt the live editor loads with, so you can read it here and then watch it move there. SNS is written on both lines and reused rather than duplicated: 6 services, one flow. Line two turns the existing SNS box into a fan-out source with three targets in column 3.
What the parser produces
6 services: API Gateway (0), Lambda (1), SNS (2), Lambda Emailer (3), Lambda Analytics (3), SQS Archive (3).
Two warnings before you go further
First, the parser never throws and never rejects. Anything it cannot interpret becomes a single box named after your text. Paste !!! total garbage ### and you get one box called !!! total garbage ###, not an error. If one of your lines produces a single box with a long name, that line is not being parsed the way you think.
Second, the editor does run a separate structural check before parsing, and it does show Invalid syntax in red for unbalanced {, ( or |, for an empty {} or ||, and for a dangling arrow. That is the full extent of the checking. Everything else is accepted silently. What that looks like →
Next: the syntax at a glance table is the whole language on one screen. Everything below it is reference.
REFERENCE
Syntax at a glance
Eight pieces of notation. That is the whole of the syntax — there are no keywords, no declarations and no header, and everything that is not one of these characters is part of a name. The language is larger than its notation, though: most of what you can express comes from how lines combine, which is what the rest of this page is about.
| Syntax | What it means | Section |
|---|---|---|
-> | Sends left to right. Splits the line into one service per segment. | Linear flows |
<- | Reverses the whole line. Direction belongs to the line, not the arrow. | Reverse arrows |
{A, B} | Fan-out. Every entry receives, at once, in the next column. | Fan-out |
(A -> B) inside { } | One fan-out entry that is a chain rather than a single service. | Chains in a fan-out |
|A, B| | Choice. Exactly one branch is taken. | Branches |
| newline | Another line of the same picture. A repeated name is the same service. What the line does depends on which name it reuses and where that name sits. | Multiple lines, convergent entry points, loops |
Namespace: | Only AWS:, User: and AI:. Chooses the icon vocabulary; stripped from the name. | Namespaces |
[x, y] | Parsed, stored on the service, and read by nothing. It moves no boxes. | Coordinates |
Whitespace around arrows and commas is optional and is trimmed. Blank lines are discarded before parsing and carry no meaning. There is no comment syntax, no quoting, no edge labels and no way to declare a service’s type — see when it goes wrong for what happens if you try.
REFERENCE
Linear flows
The smallest diagram is a name on its own. Connect services with ->: the parser splits the line on arrows and treats each piece as one service.
A column is the horizontal slot a service is drawn in. It is a 0-based integer and the leftmost column is 0. Within a line the first service is column 0 and every -> adds one. Column comes from position in the line, not from the name and not from what kind of service the parser thinks it is. Fan-out, choice and multi-line diagrams change how columns are allocated.
A name is everything between two arrows with the outer whitespace trimmed. Spaces inside are kept exactly as typed, including runs of them, so multi-word names need no quoting. Three things are taken out of a name before it is stored: a leadingnamespace: prefix, a trailing [x, y] pair, and the arrows themselves. Everything else — digits, hyphens, parentheses, commas, punctuation — is kept unchanged. The parser validates nothing.
One service
Lambda
The smallest valid diagram. One service, column 0, no targets, and a single flow with no connections. That is the whole document.
What the parser produces
1 service: Lambda (column 0).
Multi-word names
Order API -> Order Queue -> Order Worker -> Orders Table
Spaces inside a name are ordinary and no quoting is needed. Columns are 0, 1, 2, 3. The icon is picked by a case-insensitive keyword search inside the name, which is why Order Queue renders as SQS and Orders Table as DynamoDB, while Order Worker falls back to the generic icon.
What the parser produces
4 services: Order API (0), Order Queue (1), Order Worker (2), Orders Table (3).
Whitespace is trimmed at the edges
Lambda Producer -> SNS Topic
Leading and trailing spaces on the line and around each arrow are removed. Spaces inside a name are not: this box is named Lambda Producer, keeping all four interior spaces exactly as typed.
What the parser produces
2 services: Lambda Producer (0, four interior spaces kept), SNS Topic (1).
Spaces around the arrow are optional
Lambda->SQS
Exactly the same result as Lambda -> SQS. Tabs work too. Space around arrows is for your eyes only.
What the parser produces
2 services: Lambda (0), SQS (1).
The same name twice on one line makes two boxes
Lambda -> SQS -> Lambda
Three boxes, not two, and not a cycle back to the first. Within a single line the parser appends a new service per segment and never looks up an existing one by name. Name-based reuse only starts on the second and later lines — see multiple lines.
What the parser produces
3 services: Lambda (0), SQS (1), Lambda (2).
REFERENCE
Fan-out with { }
A fan-out sends one source to several targets at once. Write the targets as a comma-separated list in braces. Each entry becomes its own service and all of them land in the column immediately after the source. The source is flagged isFanOutSource; every service produced by the group is flagged isFanOutTarget.
Entries are laid out top to bottom. Each gets a row, numbered from 0 in the order you wrote it, and every service in the group gets the same totalRows. The source itself has no row and no totalRows — those fields are undefined on it, which is meaningful rather than missing: it marks a service that sits outside any row group.
| Field | On the source | On each entry |
|---|---|---|
column | n | n + 1, shared by every entry |
row | undefined | 0, 1, 2 … in written order |
totalRows | undefined | number of entries between the braces |
targets | every entry, in written order | empty, unless something follows the closing brace |
isFanOutSource | true | — |
isFanOutTarget | — | true |
Fan out to three targets
SNS -> {Lambda A, Lambda B, SQS}
Four services. SNS sits at column 0 with targets: [1, 2, 3]. The three targets share column 1 with rows 0, 1, 2 and totalRows: 3. All three edges carry isFanOut: true and a fanOutIndex equal to the entry’s row.
What the parser produces
4 services: SNS (0), Lambda A (1, row 0), Lambda B (1, row 1), SQS (1, row 2).
Rejoin after the fan-out
SNS -> {Lambda A, Lambda B} -> SQS
Anything after the closing brace receives from every branch: Lambda A and Lambda B both get targets: [3]. SQS lands at column 2 and has no row or totalRows — continuations are not part of the row group.
What the parser produces
4 services: SNS (0), Lambda A (1, row 0), Lambda B (1, row 1), SQS (2).
Fan out from a fan-out target, on a new line
EventBridge -> {Lambda Orders, Lambda Billing}
Lambda Orders -> {SQS Orders, DynamoDB Orders}
Lambda Billing -> {SQS Billing, DynamoDB Billing}
This is how you get a second level. Braces do not nest, so each later line names an existing service and fans out from it. Lambda Orders and Lambda Billing end up flagged both isFanOutTarget and isFanOutSource. Row numbering restarts per group: SQS Orders and SQS Billing are both row 0 of totalRows: 2 at column 2.
What the parser produces
7 services: EventBridge (0), Lambda Orders (1), Lambda Billing (1), SQS Orders (2), DynamoDB Orders (2), SQS Billing (2), DynamoDB Billing (2).
Does not work inside { }
- Braces inside braces.
SNS -> { {Lambda A, Lambda B}, SQS }splits on the commas and ignores the inner braces, giving services literally named{Lambda AandLambda B}. Use a second line instead. - A chain without parentheses.
SNS -> { SQS -> Lambda, DynamoDB }produces one service namedSQS -> Lambda. Parentheses are mandatory. - Pipes inside braces.
SNS -> { |DynamoDB, SQS|, Kinesis }gives ordinary fan-out targets named|DynamoDBandSQS|. There is no choice behaviour. - Two fan-outs on one line.
SNS -> {Lambda A, Lambda B} -> {SQS, DynamoDB}chains: every terminal of the first fan-out becomes a fan-out source for the second, so Lambda A and Lambda B each fan out to SQS and DynamoDB. - An empty group.
SNS -> {}is caught by the editor’s pre-parse check and shows Invalid syntax. The parser itself would accept it and draw a single node.
A trailing comma is fine, a single-entry group works, and there is no cap on the number of entries. Coordinates and namespaces survive inside a group; the comma inside [x, y] is not treated as an entry separator.
REFERENCE
Chains inside a fan-out
An entry can be a chain instead of a single service. Wrap it in parentheses. The whole parenthesised chain counts as one entry, so it occupies one row, and its services march across columns from the group’s column rightwards. The fan-out source targets only the first service of each entry; the rest of the chain is wired link by link. Chain length is not capped.
Nesting stops at two levels: one brace group, and one parenthesised chain per entry. Parentheses do not nest inside parentheses; the parser strips exactly one outer pair. None of this is an error — the stray punctuation is folded into service names, so check your names if a group looks wrong.
A chain inside one branch
SNS -> { (SQS -> Lambda), DynamoDB }
The parenthesised chain is one entry, so there are two rows, not three. SQS and Lambda both carry row 0, DynamoDB carries row 1, and all three carry totalRows: 2. SNS targets only the head of each entry: targets: [1, 3].
What the parser produces
4 services: SNS (0), SQS (1, row 0), Lambda (2, row 0), DynamoDB (1, row 1).
Branches of different lengths
SNS -> { (SQS -> Lambda -> DynamoDB), S3 }
Branches do not have to be the same length. The chain spans columns 1, 2 and 3 on row 0; S3 stays at column 1 on row 1. totalRows is 2 for every service in the group.
What the parser produces
5 services: SNS (0), SQS (1), Lambda (2), DynamoDB (3), S3 (1).
Several chains, then a continuation
SNS -> { (SQS -> Lambda), (Kinesis -> Firehose), DynamoDB } -> S3
Three entries, three rows. S3 receives from the terminal of each branch — Lambda, Firehose and DynamoDB — not from the entries’ heads. Its column is the group’s starting column plus the length of the longest branch, so it sits at column 3 whatever the shorter branches do.
What the parser produces
7 services: SNS (0), SQS (1), Lambda (2), Kinesis (1), Firehose (2), DynamoDB (1), S3 (3).
Extend one branch on a later line
SNS -> { (SQS -> Lambda), DynamoDB }
DynamoDB -> Athena
Naming a fan-out target on a later line appends to that branch. Athena lands at column 2 and inherits row 1 of totalRows: 2 from DynamoDB, keeping it on the same horizontal track. Do not combine this with a continuation after the closing brace on the first line: the later line replaces the branch’s targets rather than adding to them, and the edge into the continuation disappears. Pick one or the other.
What the parser produces
5 services: SNS (0), SQS (1), Lambda (2), DynamoDB (1), Athena (2).
REFERENCE
Branches with | |
A choice is a routing decision: one input, several mutually exclusive outputs. Write the alternatives between vertical bars, separated by commas. Compare with fan-out { }, which means “all of these at once”; choice means “exactly one of these”.
The service before the bars is marked isChoiceSource and its targets becomes the list of every branch. Each service inside the bars is marked isChoiceTarget and gets a row and a totalRows. All branches share one column; the source sits one column left, the continuation one column right. In the flow graph the source-to-branch connections carry isChoice: true; the rejoin connections do not.
Branches get interesting when they need more than one step. Put each branch on its own line, starting with the name of the branch service. A line whose first service is an existing choice target extends that branch inside the same flow rather than starting a new one, and the new services inherit that branch’s row, which is what keeps each branch in its own horizontal lane. They are also flagged isBranchContinuation. Names are matched exactly and case-sensitively, so writing the same join name on several branch lines produces exactly one service — that is what lets branches rejoin.
A choice in one line
Lambda -> |DynamoDB, SQS| -> EventBridge
The minimal choice. Lambda is the choice source with targets: [1, 2]. DynamoDB (row 0) and SQS (row 1) both carry isChoiceTarget and totalRows: 2, and both target EventBridge. Branches share column 1, the source is column 0, the join is column 2.
What the parser produces
4 services: Lambda (0), DynamoDB (1, row 0), SQS (1, row 1), EventBridge (2).
A choice with no continuation
Lambda -> |DynamoDB, SQS, SNS|
You do not have to rejoin, and the number of branches is not fixed. Nothing follows the closing bar, so all three branches end with targets: []. This is a complete, valid diagram.
What the parser produces
4 services: Lambda (0), DynamoDB (1, row 0), SQS (1, row 1), SNS (1, row 2).
Branches that continue on their own lines
Validator -> |Authorize, Reject| -> CloudWatch
Authorize -> Process -> DynamoDB -> CloudWatch
Reject -> SQS -> SNS -> CloudWatch
Lines 2 and 3 start with names that already exist as choice targets, so they extend those branches instead of starting new flows. CloudWatch is named three times and exists exactly once. Eight services, one flow. Process and DynamoDB inherit row 0 from Authorize; SQS and SNS inherit row 1 from Reject. Note that the -> CloudWatch on line 1 is overridden: Authorize ends up targeting Process, not CloudWatch.
What the parser produces
8 services, in parser order: Validator (0), Authorize (1), Reject (1), CloudWatch (2), Process (2), DynamoDB (3), SQS (2), SNS (3).
Branches that never rejoin
API -> |Process, Reject|
Process -> DynamoDB -> SNS
Reject -> Dead Letter
Branches are not required to converge. Six services, one flow, two dead ends: SNS and Dead Letter both finish with targets: [].
What the parser produces
6 services: API (0), Process (1), Reject (1), DynamoDB (2), SNS (3), Dead Letter (2).
Three-way routing with unequal branch lengths
Order API -> |Approve, Review, Decline| -> Audit Log
Approve -> Charge Card -> Ship Order -> Audit Log
Review -> Manual Queue -> Audit Log
Decline -> Notify Customer -> Audit Log
Branches may be different lengths. The long branch reaches column 3, the short ones stop at column 2, and all three land on the single Audit Log service. Nine services, one flow. Branch-line order does not matter: rows follow the order written on the choice line.
What the parser produces
9 services, in parser order: Order API (0), Approve (1), Review (1), Decline (1), Audit Log (2), Charge Card (2), Ship Order (3), Manual Queue (2), Notify Customer (2).
Naming the join only on the branch lines
Validator -> |Authorize, Reject|
Authorize -> Process -> CloudWatch
Reject -> SQS -> CloudWatch
Where you first name the join changes the layout. Here CloudWatch is created by the Authorize branch line, so it inherits row 0 of totalRows: 2 and is flagged isBranchContinuation — it renders in the top lane rather than centred. Name the join on the choice line instead and it is created outside any branch, with row and totalRows undefined, and renders centred against the group. The edges are the same either way; the layout is not.
What the parser produces
6 services, in parser order: Validator (0), Authorize (1), Reject (1), Process (2), CloudWatch (3), SQS (2).
Fan-out on one branch
Validator -> |Authorize, Reject| -> CloudWatch
Authorize -> {Process, Archive}
Reject -> CloudWatch
A branch line can fan out. Authorize becomes both isChoiceTarget and isFanOutSource, targeting Process and Archive. The Reject branch still reaches CloudWatch. Note that Authorize no longer targets CloudWatch: the fan-out replaced that edge.
What the parser produces
6 services: Validator (0), Authorize (1), Reject (1), CloudWatch (2), Process (2), Archive (2).
Fan-out after a choice, on the same line
Lambda -> |DynamoDB, SQS| -> {SNS, EventBridge}
A { } immediately after a choice attaches to every branch, not just the last one: both DynamoDB and SQS become isFanOutSource and both target the same two services. The reverse order is not symmetric — see the block below.
What the parser produces
5 services: Lambda (0), DynamoDB (1), SQS (1), SNS (2), EventBridge (2).
Nested choice on a branch line
API Gateway -> SQS -> EventBridge -> |Lambda SES, Lambda Store|
Lambda Store -> DynamoDB -> Lambda Miloljub -> S3
Lambda SES -> EventBridge 2 -> |Lambda Check, Lambda Two| -> SES
A branch line can open its own choice. EventBridge 2 is created on the Lambda SES branch and is simultaneously isChoiceTarget, isBranchContinuation and isChoiceSource, with Lambda Check and Lambda Two below it, both rejoining at SES. Twelve services, one flow. Nesting is verified three levels deep.
What the parser produces
12 services, in parser order: API Gateway, SQS, EventBridge, Lambda SES, Lambda Store, DynamoDB, Lambda Miloljub, S3, EventBridge 2, Lambda Check, Lambda Two, SES.
Does not work inside | |
The contents of the bars are split on every comma with no awareness of brackets, braces or parentheses. Keep branch entries to plain names and put the structure on the next line.
- Two choices on one line.
A -> |B, C| -> |D, E|chains: B and C each become a choice source for the second choice, so both branch to D and E. - An inline chain. Parentheses build chains inside
{ }but not inside| |.Lambda -> |(A -> B), C| -> Dgives one branch literally named(A -> B). - A nested fan-out.
Lambda -> |{A, B}, C|tears the braces in half and gives branches named{AandB}. - Arrows between the bars.
A -> |B -> C, D|gives a branch literally namedB -> C. Arrow splitting is suppressed between pipes. - Coordinates. The comma inside
[x, y]is treated as a branch separator, soLambda -> |A [0.1, 0.2]|produces branches namedA [0.1and0.2]. - Empty or trailing entries.
A -> |B, C,|produces a real branch with an empty name at row 2. Empty entries are not skipped; they become invisible boxes and inflatetotalRows. - Two continuation lines from the same branch. Writing
B -> Dand thenB -> Egives B both targets. A later line replaces an existing edge only when it rejoins that same service — which is how branch expansion works, whereB -> Process -> CloudWatchreplaces the direct B-to-CloudWatch shortcut. - A choice directly after a fan-out.
A -> {B, C, D} -> |E, F|is not the mirror of the working case above: only the last fan-out target becomes the choice source, and B and C end with no targets.
REFERENCE
Multiple lines
The parser reads the prompt top to bottom, one line at a time. Blank and whitespace-only lines are discarded before parsing and carry no meaning, so you can use them freely for readability.
From the second line onwards, a name that already exists is reused rather than duplicated. That single rule is what makes multi-line diagrams work: it is how branches rejoin, how a chain converges, and how you attach a fan-out to something you drew earlier. Matching is by exact display name and is case-sensitive.
What a flow is
A flow is one animation pass over the diagram, and the parser reports how many your prompt produced. Flows play strictly in order: every message in flow 0 must arrive before flow 1 starts, and there is a half-second pause between them. That is the whole meaning of “one flow” and “two flows” in the parser output quoted throughout this page — two flows means the second group of arrows waits its turn rather than moving with the first.
You do not choose the flow. The parser decides it for each line after the first, from what that line touches, and it tests these in order:
- · It continues an existing branch — the line starts with a name that is already a choice or fan-out target. Stays in flow 0. See branches.
- · It converges — the line brings new services that feed into something you already drew. Merges into flow 0 and animates at the same time as it. See convergent entry points.
- · It points backwards — the line ends on a service that already sits further left. Becomes a later flow that retraces the forward path. See loops and return paths, which also covers the one combination where this rule does not apply.
- · Anything else becomes a later flow of its own, including a line that shares no name at all with what you have drawn.
One thing worth knowing before you read the parser output: targets is not always the whole graph, and which case you are in matters. A line that continues a branch rewrites that service’s targets in place, replacing whatever an earlier line put there, and its edges stay in flow 0. A line that starts a fresh chain instead records its edges only in a new entry of flows and leaves targets alone — which is why API Gateway -> Lambda followed by Lambda -> DynamoDB leaves Lambda with targets: [] and produces two flows.
A repeated name is the same service
API Gateway -> Lambda
Lambda -> DynamoDB
Three services, not four. Line two starts with a name that already exists, so it continues from the existing Lambda instead of creating a second one. DynamoDB lands at column 2. Contrast with Lambda -> SQS -> Lambda on a single line, which really does draw two Lambdas. Line two is a fresh chain rather than a branch continuation, so it becomes a second flow: the arrow into DynamoDB animates after the arrow into Lambda, and Lambda’s own targets stays empty.
What the parser produces
3 services: API Gateway (0), Lambda (1), DynamoDB (2). Two flows.
A line that shares no name draws a separate diagram
A -> B -> C
D -> E
This is how you put two unrelated pipelines on one canvas. Line two names nothing that already exists, so it joins the picture only in the sense of sharing the canvas: there is no connection of any kind between the two groups. D restarts at column 0 alongside A, and the second chain animates only after the first has finished.
What the parser produces
5 services: A (0), B (1), C (2), D (0), E (1). Two flows.
Reuse is case-sensitive
Order API -> |Approve, Decline| -> CloudWatch
Approve -> cloudwatch
Won’t work as a rejoin. cloudwatch is not CloudWatch, so you get five services with two separate boxes that look like the same thing. Name lookup uses exact string equality. If a branch refuses to converge, check the capitalisation first.
What the parser produces
5 services: Order API (0), Approve (1), Decline (1), CloudWatch (2), cloudwatch (2).
Deduplication starts on line two. On the first line every segment creates a new service, so Z -> A -> |B, C| -> Z gives you two boxes called Z, and SNS -> {A, A} gives you two called A.
REFERENCE
Convergent entry points
This is fan-in: several producers arriving at one service. Write each extra entry point on its own line, ending on a name that already exists. Because the new line feeds something you already drew, the parser folds it into flow 0 rather than giving it a flow of its own, so both entry points animate together instead of one after the other. That is the difference between fan-in and two unrelated chains.
Placement is worked out for the whole diagram at once, not line by line. Each service is pushed as far right as its longest incoming path requires; a converging source that would otherwise be left with a gap in front of it is pulled in to sit exactly one column left of its target and flagged isConvergentSource. If that would put a column below zero, every column shifts right instead. Services that end up sharing a column then get a row and a totalRows so they stack rather than overlap.
Because placement is global, the order of your lines does not change the columns. You can write the converging line before the main chain and get the same layout, which is worth knowing when you are pasting a diagram together out of order.
A second entry point converging on an existing service
API Gateway -> Lambda -> DynamoDB
EventBridge -> Lambda
The smallest fan-in. Line two introduces a new service that feeds an existing one, so the result is a single flow with two roots — API Gateway and EventBridge — and a message from either entry point continues downstream through Lambda to DynamoDB. EventBridge already lands adjacent to Lambda, so it is not flagged isConvergentSource and nothing moves. It does share column 0 with API Gateway, so both pick up a row and stack.
What the parser produces
4 services: API Gateway (0, row 0 of 2), Lambda (1), DynamoDB (2), EventBridge (0, row 1 of 2). One flow, two roots.
More than one converging line
A -> B -> C -> D
E -> C
F -> D
There is no limit on how many entry points converge. E and F each land one column left of the service they feed — column 1 and column 2, not column 0 — and both are flagged isConvergentSource. E shares column 1 with B and F shares column 2 with C, so each of those pairs stacks as rows 0 and 1. Still one flow, now with three roots: A, E and F all start moving at once.
What the parser produces
6 services: A (0), B (1, row 0 of 2), C (2, row 0 of 2), D (3), E (1, row 1 of 2), F (2, row 1 of 2). One flow, three roots.
Line order does not change the columns
E -> C
A -> B -> C -> D
The same diagram as A -> B -> C -> D followed by E -> C, written the other way round. Every column is identical: A at 0, B and E at 1, C at 2, D at 3. Writing the converging line first does not push the main chain rightwards and does not leave E stranded at column 0. Only which of B and E is row 0 follows the order you wrote.
What the parser produces
5 services, in parser order: E (1, row 0 of 2, convergent source), C (2), A (0), B (1, row 1 of 2), D (3). One flow, two roots.
Convergence carries through the chain
A -> B -> C
D -> E -> C
F -> E
Line two converges on C. Line three converges on E, which line two had only just created — and that is enough to pull it into the same picture. All three entry points end up in one flow with three roots. A, D and F share column 0 and stack as rows 0, 1 and 2 of totalRows: 3; B and E share column 1. Nothing here is flagged isConvergentSource, because every source already sits directly in front of its target.
What the parser produces
6 services: A (0, row 0 of 3), B (1, row 0 of 2), C (2), D (0, row 1 of 3), E (1, row 1 of 2), F (0, row 2 of 3). One flow, three roots.
REFERENCE
Loops and return paths
Point a later line back at a service that already sits further left and you get a return path. The parser does not draw one long arrow flying backwards over your diagram. It looks up the forward route between the two services and re-emits that route reversed, one hop at a time, as a new flow. The message retraces the path it came by.
This is the mechanism behind an agent loop, and you write it as one ordinary line. The rule has one exception: if the two services are already neighbours there is nothing to expand, so you get a single backward arrow — expansion needs a forward path of at least three services. Where that single arrow lands depends on where the line starts, and one combination stops the animation dead. The last example below is the one to read before you draw a loop.
An agent calling a tool and coming back
User -> Agent -> LLM -> |Tool, Response|
Tool -> Agent
Line two adds no boxes. It draws two arrows, not one: Tool back to LLM, then LLM back to Agent, retracing the forward route. They live in a second flow, so the outbound pass finishes first and the return then plays over the same boxes. Writing Tool -> Agent and expecting a single long arrow is the surprise this section exists for.
What the parser produces
5 services: User (0), Agent (1), LLM (2), Tool (3, row 0 of 2), Response (3, row 1 of 2). Two flows; the second carries Tool→LLM and LLM→Agent.
The return is as long as the path it retraces
A -> B -> C -> D -> E
E -> B
Three hops back, not one: E to D, D to C, C to B. The expansion follows the forward route exactly, so a longer chain gives a longer return. No new services and no new columns — only a second flow.
What the parser produces
5 services: A (0), B (1), C (2), D (3), E (4). Two flows; the second carries E→D, D→C and C→B.
Neighbours stay a single arrow
A -> B -> C
B -> A
The exception. A and B are adjacent, so there is no intermediate service to route through and the second flow holds exactly one arrow, B back to A. This is also how you draw a plain request-and-response pair over two boxes.
What the parser produces
3 services: A (0), B (1), C (2). Two flows; the second carries B→A alone.
A one-hop loop back from a branch stops the animation
Validator -> {Authorize, Reject}
Authorize -> Validator
Won’t work, and it fails without warning. A line starting from a service inside { } or | | continues that branch, so its edge is written into targets and stays in flow 0 — it does not become a return flow. Here that closes a cycle, and a flow whose every arrow has an arrow feeding it has no arrow to start from: the diagram draws, and then nothing moves at all. Contrast A -> B / B -> A, where B is an ordinary service and you get two working flows. Loop back to a service two or more hops away, or loop back from outside a branch.
What the parser produces
3 services: Validator (0), Authorize (1, row 0 of 2), Reject (1, row 1 of 2). One flow, and it has no roots — nothing animates.
REFERENCE
Reverse arrows
<- writes the same flow backwards. The rule is mechanical: the parser splits the line into segments and then, if the line contained at least one <- anywhere, it reverses the entire list of segments. Use it when a return path reads better right to left.
That rule has a consequence worth internalising. Direction is a property of the line, not of each individual arrow. Mixing -> and <- on one line does not give you a round trip; it gives you one chain, reversed. Reversal is per line, so another line’s direction is unaffected. Keep one arrow direction per line, and if you want a request and a response, write two lines — see step 4.
Reverse arrow
API Gateway <- Lambda <- DynamoDB
Identical to DynamoDB -> Lambda -> API Gateway: same services in the same order, same columns, same connections. DynamoDB is column 0 and API Gateway is column 2.
What the parser produces
3 services, in parser order: DynamoDB (0), Lambda (1), API Gateway (2).
Mixing arrow directions reverses the whole line
Client -> API Gateway <- Lambda
Won’t work as a round trip. One <- anywhere on the line reverses every segment on that line, so the result is the chain Lambda → API Gateway → Client — the opposite of what the text reads like. There is no bidirectional edge in the language.
What the parser produces
3 services, in parser order: Lambda (0), API Gateway (1), Client (2).
REFERENCE
Namespaces
Every box gets an icon, a colour and a small type label above it, and you never choose those directly — they come from a keyword search inside the name. A namespace prefix overrides that search by telling the parser which keyword vocabulary to look in. Use one when the default search picks the wrong thing, and only then: the language needs no prefix to work, and most of the examples on this page do not use one.
The prefix is stripped before anything else happens, so AWS:Lambda produces a service named Lambda and that is what is drawn under the box. The namespace is recorded separately on the service and is not drawn anywhere. It does not group, tint, box or lane anything.
Only three namespaces exist
AWS, User and AI. They are case-sensitive and there is no way to add more — they are registered in code when the editor starts and there is no UI for them. Any other prefix, including a mistyped aws:, is not an error and produces no warning: it parses, the prefix is still stripped from the name, and the box silently falls back to the generic grey dot. If a box you expected to be orange came out grey, check the prefix first.
The colon rule is blunt: the first colon on a segment always splits off a namespace, whatever came before it. Only the first, so AWS:Service:WithColon gives the name Service:WithColon. A name cannot contain a colon — do not use one for anything except the three real namespaces.
Prefix a service with its namespace
User:Browser -> API Gateway -> Lambda -> DynamoDB
User:Browser renders as a Browser and is named Browser — the prefix is stripped from the label. The other three segments need no prefix: the bare-name search finds api in the AWS list, then lambda, then dynamodb. Mixing prefixed and unprefixed segments on one line is normal.
What the parser produces
4 services: Browser (0, namespace User), API Gateway (1), Lambda (2), DynamoDB (3).
Without a prefix, AWS wins the tie
API -> Memory
A bare name is tried against the three lists in order — AWS, then User, then AI — and the first list containing a match wins. api is a keyword in both AWS (API Gateway) and AI (Tool), so API becomes an API Gateway. Memory matches nothing in AWS or User, so the search falls through to AI.
What the parser produces
2 services: API (0, API Gateway), Memory (1, Memory).
Force the AI vocabulary with a prefix
AI:API -> AI:Memory
Same two names, one prefix. AI:API now searches only the AI list and becomes a Tool instead of an API Gateway. This is the case a namespace is actually for: overriding a keyword collision.
What the parser produces
2 services: API (0, namespace AI, Tool), Memory (1, namespace AI, Memory).
An unregistered namespace silently goes generic
AWS:Lambda -> SNS -> Google:Functions
Won’t work. Google is not one of the three registered namespaces, so no keyword list is searched at all and the box renders as the generic grey dot labelled Service. It is not an error and there is no warning — the service is still created, still named Functions, still connected. Without the prefix, bare Functions would have matched the AWS keyword function and drawn a Lambda.
What the parser produces
3 services: Lambda (0), SNS (1), Functions (2, namespace Google, generic).
Namespaces are case-sensitive
aws:Lambda -> AWS:Lambda
Won’t work: aws: is not AWS:. The first box falls back to generic grey; the second is a proper orange Lambda. Both are named Lambda — and because this is a single line, they really are two separate boxes. This is the single most likely reason a box came out grey when you expected an icon.
What the parser produces
2 services: Lambda (0, namespace aws, generic), Lambda (1, namespace AWS, Lambda).
A name identifies a box; a namespace does not
AWS:Lambda -> SQS
AI:Lambda -> DynamoDB
Three services, not four. Line two’s AI:Lambda resolves to the name Lambda, which already exists, so it merges into the box created on line one and keeps that box’s AWS namespace and its icon. There is no way to have two different boxes both named Lambda across lines.
What the parser produces
3 services: Lambda (0, namespace AWS), SQS (1), DynamoDB (1).
REFERENCE
Coordinates [x, y]
A name may carry a trailing [x, y] suffix. It parses, it is stored on the service, and the bracketed text is removed from the drawn name. Values may be numbers or bare words; nothing is validated, range-checked or clamped.
Coordinates do not move anything
The layout engine places boxes purely by column and row, computed from the arrows you wrote. It never reads the coordinate values — nothing in the shipped product does. Treat [x, y] as inert metadata, not as positioning. If you want a box further right, put more arrows in front of it.
Coordinates parse and are stored
Lambda [0.5, 0.7] -> SQS [0.8, 0.7]
Two services named Lambda and SQS, each carrying a coordinates object. The bracketed text is not part of the name and is not drawn.
What the parser produces
2 services: Lambda (0, coordinates x 0.5 y 0.7), SQS (1, coordinates x 0.8 y 0.7).
Coordinates do not move anything
Lambda [0.9, 0.9] -> SQS [0.1, 0.1]
Won’t work as positioning. Lambda claims x 0.9 and SQS claims x 0.1, so you might expect SQS on the left. It is not: Lambda stays at column 0 and SQS at column 1, and the rendered pixel positions are identical to a plain Lambda -> SQS. Layout is decided by arrow order alone.
What the parser produces
2 services: Lambda (column 0), SQS (column 1) — unchanged by the coordinates.
Namespace and coordinates combined
AWS:Lambda [0.2, 0.3] -> AWS:SQS [0.5, 0.3]
Prefix first, brackets last, both stripped from the name. The icons come from the AWS list; the coordinates change nothing on screen. Given that, the only reason to write them today is to carry a note you intend to use later.
What the parser produces
2 services: Lambda (0, namespace AWS, coordinates x 0.2 y 0.3), SQS (1, namespace AWS, coordinates x 0.5 y 0.3).
Brackets swallow everything after them
My [x] Lambda
Won’t work as a name. This is one box named My, not My Lambda. Everything from the first [ onward is discarded, so the trailing word is thrown away with it — and with it the keyword that would have made this a Lambda box. It renders generic grey. Keep brackets at the end of a name or leave them out.
What the parser produces
1 service: My (column 0, generic).
A missing comma silently yields no coordinates at all while still deleting the bracketed text from the name: Lambda [0.5] is a service named Lambda with coordinates: null. Word coordinates such as [Product, 0.7] are stored as the literal string you typed; nothing resolves them to a number in the shipped editor, so do not build a map on them.
REFERENCE
How a name becomes an icon
Case-insensitive whole-word matching against a fixed keyword list, first match wins, and a trailing s is tolerated. That is the whole mechanism. Order Table becomes a DynamoDB box because one of its words is table, and Responses matches response. If nothing matches you get the generic grey dot labelled Service, and your name still renders exactly as typed.
An AI pipeline needs no prefixes at all
User -> Agent -> LLM -> Vector DB
Every name here is found by the bare-name search. User matches the AI keyword user and renders as Human — note that the User namespace does not contain the word user. Agent gives AI Agent, LLM gives LLM, Vector DB gives Vector DB.
What the parser produces
4 services: User (0, Human), Agent (1, AI Agent), LLM (2, LLM), Vector DB (3, Vector DB).
Keywords work inside ordinary business names
Order Table -> Audit Stream -> Search Index
This is the mechanism working for you: table gives DynamoDB, stream gives Kinesis, search gives an AI Tool. You get sensible icons without naming AWS services at all.
What the parser produces
3 services: Order Table (0, DynamoDB), Audit Stream (1, Kinesis), Search Index (2, Tool).
A whole word can still hide in a name
Webhook -> Responses -> User Service
A keyword only matches a whole word, so a short keyword can no longer hide inside a longer one — Webhook is not a Browser and Approved is not a phone. What still catches people is a keyword that is actually one of the words: User Service renders a Human because user is a word in it, and Order Queue renders an SQS queue. If a name comes out wrong, look for a keyword among its words, then rename the box or force the namespace.
What the parser produces
3 services: Webhook (0, Browser), Responses (1, SES), User Service (2, Human).
AWS keywords
Matched left to right; the first keyword found as a whole word in the name wins.
| Keywords | Type | Icon |
|---|---|---|
lambda, function | Lambda | λ |
sqs, queue | SQS | ☰ |
sns, notification, topic | SNS | 📢 |
dynamodb, table | DynamoDB | ⬡ |
s3, bucket, storage | S3 | 🥣 |
api, gateway, rest, http | API Gateway | ⇆ |
kinesis, stream | Kinesis | ≋ |
eventbridge, events | EventBridge | ⚡ |
step, workflow, state machine | Step Functions | ⎔ |
ses, email | SES | 📧 |
User keywords
| Keywords | Type | Icon |
|---|---|---|
browser, web, desktop | Browser | 🌐 |
mobile, app, phone, ios, android | Mobile | 📱 |
Note that user is not a User keyword. A bare User matches the AI list and renders as a Human.
AI keywords
| Keywords | Type | Icon |
|---|---|---|
agent, assistant, bot, coordinator, orchestrator, router, supervisor, researcher, writer, evaluator, critic, planner, executor, triage, reranker | AI Agent | 🤖 |
llm, model, gpt, claude, gemini, openai, anthropic, mistral, llama, generator | LLM | 🧠 |
tool, function call, action, plugin, mcp, api, search, retriever, scraper | Tool | 🔧 |
vectordb, vector, embedding, pinecone, chroma, qdrant, pgvector, weaviate, database, knowledge graph | Vector DB | ◈ |
memory, context, history, conversation, cache | Memory | 💾 |
guardrail, filter, safety, moderation, validator | Guardrail | 🛡 |
human, reviewer, person, operator, user | Human | 👤 |
Two AI keywords are shadowed by AWS and are only reachable with an explicit AI: prefix: api (bare, it is an API Gateway) and function call (bare, the AWS keyword function matches first and you get a Lambda). There is no way to declare a box’s type explicitly — the wording of the name and the namespace prefix are the only levers you have. A name longer than 14 characters is drawn as its first 12 characters plus an ellipsis; the full name is still what the parser stores and what a later line has to match.
TROUBLESHOOTING
When it goes wrong
The parser never rejects anything, and this is the one thing that will bite you. It has no error path. A line it does not recognise as containing an arrow is not flagged, skipped or reported — it silently becomes a single service whose name is the whole line.
The sign to look for is a box you did not intend, sitting at column 0 with nothing connected to it, whose label is a whole line of your source. When you see that, check the arrow on that line.
The editor does run one structural check before parsing and will show Invalid syntax in red for an unbalanced {, ( or |, for an empty {} or ||, and for a dangling arrow such as Lambda ->. That is the full extent of the checking. Everything on this page passes it and is still wrong.
Prose is a service name
!!! total garbage ###
One box, named after your text, at column 0. No error, no warning, and it passes the editor’s pre-parse check. Anything you type that contains no arrow does this, including an English sentence.
What the parser produces
1 service named !!! total garbage ### (column 0, no targets).
=> is not an arrow
Lambda => SQS
Won’t work. One service named Lambda => SQS. The only two arrows are -> and <-. A Unicode → and a spaced - > fail the same way.
What the parser produces
1 service named Lambda => SQS (column 0).
A near-miss arrow is worse than a wrong one
Lambda --> SQS
Won’t work, and this half-works, which is why it is easy to miss. The parser matches the -> at the end and keeps the leading hyphen as part of the name, producing a service called Lambda -. The connection is made, so the diagram looks nearly right; only the label is wrong.
What the parser produces
2 services: Lambda - (0) and SQS (1). Note the trailing hyphen in the first name.
There is no comment syntax
# this is a note
API Gateway -> Lambda
Won’t work. The heading becomes a stray box at column 0 in its own flow. // behaves the same way, and a trailing comment welds itself onto the last service name on the line. There is no comment stripping anywhere in the parser.
What the parser produces
3 services: # this is a note (0), API Gateway (0), Lambda (1).
There are no edge labels, and quoting damages a name
A -> "publishes" -> B
Won’t work. There is no syntax for putting text on an arrow; the label becomes its own box, and the quote characters are kept and drawn. Names never need quoting — whitespace is already trimmed — and quoting actively damages the label.
What the parser produces
3 services: A (0), "publishes" (1, quotes included), B (2).
Square brackets around a name produce an invisible box
API Gateway -> [Lambda] -> DynamoDB
Won’t work, and this is the worst outcome on the list. Square brackets are the coordinate syntax, not a label syntax, so the middle service ends up with an empty name: a real, connected, unlabelled box in the middle of your diagram.
What the parser produces
3 services: API Gateway (0), an empty name (1), DynamoDB (2).
A colon anywhere creates a namespace
Step 1: Validate -> Step 2: Persist
Won’t work as labels. These are not boxes called Step 1: Validate and Step 2: Persist. The first colon splits each segment, so the names are Validate and Persist, in namespaces Step 1 and Step 2. Neither is registered, so both render generic grey.
What the parser produces
2 services: Validate (0, namespace Step 1), Persist (1, namespace Step 2).
A chain in braces without parentheses
SNS -> { SQS -> Lambda, DynamoDB }
Won’t work. The arrow is swallowed into the name and you get a single box labelled SQS -> Lambda. Parentheses are mandatory for a chain inside a group.
What the parser produces
3 services: SNS (0), SQS -> Lambda (1, one box), DynamoDB (1).
Braces do not nest
SNS -> { {Lambda A, Lambda B}, SQS }
Won’t work. The group is split on every comma and the inner braces are ignored, so they end up glued to the names. Watch for a box called {Lambda A — that is the tell. Use a second line for a second level.
What the parser produces
4 services: SNS (0), {Lambda A (1), Lambda B} (1), SQS (1).
Parentheses do not build chains inside | |
Lambda -> |(A -> B), C| -> D
Won’t work, even though the same notation does work inside { }. There is no A and no B and no internal edge — just one branch literally named (A -> B). Put the chain on its own branch line.
What the parser produces
4 services: Lambda (0), (A -> B) (1, one box), C (1), D (2).
Coordinates inside a choice are torn in half
Lambda -> |A [0.1, 0.2], B [0.3, 0.4]|
Won’t work. Coordinates survive inside a fan-out but not inside a choice: the contents of the bars are split on every comma with no bracket awareness, so two branches become four and totalRows becomes 4.
What the parser produces
5 services: Lambda (0), A [0.1, 0.2], B [0.3, 0.4] (all column 1).
Two choices on one line chain through
A -> |B, C| -> |D, E|
Works. Every terminal of the first choice becomes a choice source for the second, so B and C each branch to D and E. This used to drop the first choice entirely, orphaning B and C; that was fixed, and a test now pins it.
What the parser produces
5 services: A (0), B (1), C (1), D (2), E (2) — but B and C are orphaned.
Two fan-outs on one line chain through
SNS -> {Lambda A, Lambda B} -> {SQS, DynamoDB}
Works, in the same way as the line above. Every terminal of the first fan-out becomes a fan-out source for the second, so Lambda A and Lambda B each fan out to SQS and DynamoDB. This used to overwrite the source and orphan both Lambdas; that was fixed, and a test now pins it.
What the parser produces
5 services: SNS (0), Lambda A (1), Lambda B (1), SQS (2), DynamoDB (2) — but Lambda A and Lambda B are orphaned.
THE PRODUCT
Exports
Four formats exist: animated WebM, animated WebP, an animated GIF, and a still-frame PNG of the current frame. That is the entire list, on both plans.
The three animated formats are recordings: you choose the format, press record, the diagram animates, you press stop. The GIF is the one to reach for where a video will not play — a GitHub comment, a README, a Slack message. It is around three times the size of the WebP for the same clip, and about the size of the WebM, because the format compresses every frame on its own and allows only 256 colours in each. A GIF recording has a floor and a ceiling. Stop it before it has captured two frames and you get no file and a message saying so — a one-frame GIF is a still wearing an animation’s clothes, so it is refused rather than handed to you. At the other end, frames are held in memory as raw pixels until you press stop, so a take is capped at 200 frames or 384MB of them, whichever comes first: about 20 seconds at a 960×480 canvas, and about 5 on a retina display, where each frame is four times the size. Reaching the cap is not an error — the recording stops itself, tells you, and exports what it captured. The PNG is not a recording — one press captures the frame on screen and downloads it. Every export embeds the diagram source in the file itself. Reading it back is done with Import diagram… in the editor’s settings panel, which is a signed-in feature — there is no drag-and-drop, and the landing-page editors on this site have no import control at all.
Every export carries a watermark, and it is the fixed text made by chartado. It is hardcoded; there is no setting for it and no way to remove or change it on either plan.
THE PRODUCT
Plan limits
| Free | Pro — $9/month | |
|---|---|---|
| Diagrams saved to the cloud | 3 | 100 |
| Projects | 1 | Unlimited |
| The editor itself | Never capped | Never capped |
| Exports | Animated WebM, WebP and GIF, still PNG | Animated WebM, WebP and GIF, still PNG |
| Service icons and syntax | All of them | All of them |
Write and animate as many diagrams as you like in the browser — the editor is not capped and not time-limited. The limit is on how many you can persist to the cloud so they survive a reload.
Pro cannot be purchased. There is no checkout anywhere on the site. The price above is what is published today; since nothing can be charged yet, nothing is locked in. See pricing.
THE PRODUCT
Not built
These do not exist. Not on Free, not on Pro, not behind a flag. They are listed here so you can rule the product out quickly if you need one of them.
- · MP4 and PDF export. There is no code for either of them. Animated WebM, animated WebP, animated GIF and still PNG are the only exports.
- · Team collaboration. No sharing, no invites, no collaborative editing. Today the share is the exported file.
- · A custom watermark. The text is hardcoded.
Within the language, the same is true: there are no comments, no edge labels, no quoting, no explicit type annotations, no user-defined namespaces, and [x, y] moves nothing. Everything in when it goes wrong is a thing somebody reasonably expected to work and which does not.