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, so the message goes to every entry at once, and |A, B| is a choice, so it takes one path. Layout treats them the same: both put their entries in one column with a row and a totalRows to stack by, and the only thing that differs is the animation, which the parser flags as isFanOutSource or isChoiceSource on the service in front of the bracket.
Fan-out: send to everything at once
API Gateway -> Lambda -> SNS -> {Lambda Emailer, SQS Archive}
SNS becomes the fan-out source, with targets: [3, 4] and isFanOutSource: true, and both entries land in column 3 (rows 0 and 1, totalRows: 2) and stack. Play it and the message goes down both arrows at once.
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
The pipes make Lambda the choice source (isChoiceSource: true, targets: [2, 3]) and DynamoDB and SQS are flagged isChoiceTarget, both in column 2. After the closing pipe comes the rejoin. Both branches target EventBridge. Same shape as the fan-out above, and the meaning only differs at playback, when the message takes just one of the two arrows.
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 each piece becomes one service.
A column is the horizontal slot a service is drawn in, counted from 0 at the left. Within a line the first service is column 0 and every -> adds one. Position in the line is all that decides it. Neither the name nor the type the parser guessed for it has any say. Fan-out, choice and multi-line diagrams allocate columns differently.
Names are whatever sits between two arrows, with the outer whitespace trimmed and everything inside kept exactly as typed, runs of spaces included, so multi-word names need no quoting. Before a name is stored the parser takes out a leading namespace: prefix, a trailing [x, y] pair and the arrows themselves, and nothing else: digits, hyphens, parentheses, commas and any other punctuation stay as they are. It validates none of it.
One service
Lambda
The smallest valid diagram. One service at column 0, no targets, and a single flow with no connections in it.
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, no quoting needed. Columns 0 to 3. The icon comes from a case-insensitive keyword search inside the name, which is why Order Queue renders as SQS and Orders Table as DynamoDB while Order Worker, which matches nothing, gets 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
Spaces at either end of the line and on either side of each arrow are removed. Spaces inside a name are not. This box is named Lambda Producer, all four interior spaces intact.
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. Within a single line the parser appends a new service for every segment and never looks an existing one up by name, so the second Lambda is a new box rather than a cycle back to the first. Name-based reuse starts on line two (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. The parser reports how many your prompt produced, and they play strictly in order, every message in flow 0 arriving before flow 1 starts, with a half-second pause between them, so two flows in the output quoted on this page means the second group of arrows waits for the first to finish.
You do not choose the flow. For every line after the first, the parser decides it from what that line touches, checking these in order:
- · 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).
- · Converges: the line brings new services that feed into something you already drew, so it merges into flow 0 and animates at the same time as it. See convergent entry points.
- · 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. That includes a line that shares no name at all with what you have drawn.
Read the parser output with one caveat: targets is not always the whole graph. Continue a branch and the line rewrites that service’s targets in place (whatever an earlier line put there is gone), and its edges stay in flow 0. Start a fresh chain and targets is not touched at all. Its edges go into a new entry of flows, 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 looks up the forward route between the two services and re-emits it reversed, one hop at a time, as a new flow. No long arrow flies backwards over the diagram. The message retraces the path it came by.
This is the mechanism behind an agent loop, and you write it as one ordinary line. There is one exception. Neighbours have nothing to expand (expansion needs a forward path of at least three services), so two adjacent services get a single backward arrow, and where that arrow lands depends on where the line starts. One combination stops the animation dead, and it is the last example below.
An agent calling a tool and coming back
User -> Agent -> LLM -> |Tool, Response|
Tool -> Agent
Line two adds no boxes. It draws two arrows, Tool back to LLM and then LLM back to Agent, the forward route in reverse, and both live in a second flow, so the outbound pass finishes before the return plays over the same boxes. Writing Tool -> Agent and expecting a single long arrow is the usual surprise.
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: E to D, D to C, C to B. The expansion follows the forward route exactly, so the longer the chain the longer the return, and all of it lands in a second flow with no new services and no new columns.
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, there is nothing to route through, and the second flow holds exactly one arrow, B back to A. It is also how you draw a plain request and response 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 that starts 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. Every arrow in the flow now has an arrow feeding it, so there is nowhere to start, and what you see is a diagram that draws and then never moves. Compare A -> B followed by B -> A, where B is an ordinary service and you get two working flows. Either loop back to a service two or more hops away, or start the loop from outside the 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, none of which you choose directly: all three come from a keyword search inside the name, and a namespace prefix overrides that search by telling the parser which keyword vocabulary to look in. Most of the examples on this page manage without one. A prefix is for when the default search picks the wrong thing, and only then.
Before anything else happens the prefix comes off, so AWS:Lambda produces a service named Lambda and that is what is drawn under the box. Its namespace is recorded on the service, separately, and never drawn. 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, a mistyped aws: included, still parses and is still stripped from the name, but the box silently falls back to the generic grey dot, with no error and no warning. If a box came out grey where you expected orange, check the prefix first.
Colons are handled bluntly. The first colon on a segment always splits off a namespace, whatever came before it, and only the first, so AWS:Service:WithColon gives the name Service:WithColon. Whatever came before that first colon never reaches the name, so do not use a colon 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, because the bare-name search finds api in the AWS list, then lambda, then dynamodb, and 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 with a match wins. api is a keyword in both AWS (API Gateway) and AI (Tool), so API becomes an API Gateway, while Memory matches nothing in AWS or User and 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 comes back a Tool instead of an API Gateway, which is what 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. No error, no warning. The service is still created, named Functions and connected, and had you left the prefix off, 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 and the second is a proper orange Lambda, both named Lambda, and because this is a single line they really are two separate boxes. This is the 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. 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 get 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. The parser stores it on the service and removes the bracketed text from the drawn name, and that is all it does with it: the values can be numbers or bare words, and nothing is validated, range-checked or clamped.
Coordinates do not move anything
The layout engine places boxes by column and row, which come from the arrows you wrote, and it never reads the coordinate values. Nothing in the shipped product does. Treat [x, y] as inert metadata. 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, and the bracketed text is neither part of the name nor 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, SQS at column 1, and the rendered pixel positions are identical to a plain Lambda -> SQS.
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 and the coordinates change nothing on screen, so the only reason to write them 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, the trailing word with it, and with the word goes the keyword that would have made this a Lambda box, so it renders generic grey. Brackets go at the end of a name or nowhere.
What the parser produces
1 service: My (column 0, generic).
Leave out the comma and you get no coordinates at all, though the bracketed text still comes off 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 in the shipped editor resolves them to a number, 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
Six formats exist: animated WebM, animated WebP, an animated GIF, a still-frame PNG and a still-frame PDF of the current frame, plus an MP4 (H.264) video, every one of them free. That is the entire list. WebM and PNG export from the editors on this site without an account; WebP, GIF, MP4 and PDF are in the app after a free sign-up, which costs nothing and needs no card. No export format belongs to a plan: Pro adds 100 saved diagrams and unlimited projects, and cannot be purchased yet.
The 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 and the PDF are not recordings — 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.
The PDF is a still of the current frame on a single page sized to the diagram, so there are no margins and nothing to crop. Boxes, lines and arrows are vector shapes and the labels are selectable text; the service icons are embedded as images. It carries the same watermark as every other export, and the diagram source travels inside the file, so Import diagram… reads it back like any other export.
Every export carries a watermark, and it is the fixed text made by chartado.com. 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 | Export to MP4, WebM, WebP, GIF, PNG, PDF | The same formats as Free |
| 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.
- · 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.