Chartado diagram language

A diagram is plain text. Every line becomes a row of services wired left to right, and a message travels the arrows on screen. There is nothing to install and no account needed to try it.

API Gateway -> Lambda -> DynamoDB

Every example on this page carries that same link. It goes to the editor embedded on the AWS diagram page: no login, and it is already animating when it loads. Copy the line, paste it into the prompt box — the editor accepts no diagram in the URL, because there is no prefill parameter anywhere in the code. The separate app at app.chartado.com is where saved diagrams live and it does require an account; you need nothing to read this page or to use the live editor.

Contents

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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.

Paste this in the live editor →

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).

Paste this in the live editor →

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.

SyntaxWhat it meansSection
->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
newlineAnother 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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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.

FieldOn the sourceOn each entry
columnnn + 1, shared by every entry
rowundefined0, 1, 2 … in written order
totalRowsundefinednumber of entries between the braces
targetsevery entry, in written orderempty, unless something follows the closing brace
isFanOutSourcetrue
isFanOutTargettrue

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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 A and Lambda B}. Use a second line instead.
  • A chain without parentheses. SNS -> { SQS -> Lambda, DynamoDB } produces one service named SQS -> Lambda. Parentheses are mandatory.
  • Pipes inside braces. SNS -> { |DynamoDB, SQS|, Kinesis } gives ordinary fan-out targets named |DynamoDB and SQS|. 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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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.

Paste this in the live editor →

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| -> D gives one branch literally named (A -> B).
  • A nested fan-out. Lambda -> |{A, B}, C| tears the braces in half and gives branches named {A and B}.
  • Arrows between the bars. A -> |B -> C, D| gives a branch literally named B -> C. Arrow splitting is suppressed between pipes.
  • Coordinates. The comma inside [x, y] is treated as a branch separator, so Lambda -> |A [0.1, 0.2]| produces branches named A [0.1 and 0.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 inflate totalRows.
  • Two continuation lines from the same branch. Writing B -> D and then B -> E gives B both targets. A later line replaces an existing edge only when it rejoins that same service — which is how branch expansion works, where B -> Process -> CloudWatch replaces 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.

Paste this in the live editor →

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.

Paste this in the live editor →

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).

Paste this in the live editor →

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.

Paste this in the live editor →

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.

Paste this in the live editor →

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.

Paste this in the live editor →

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.

Paste this in the live editor →

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.

Paste this in the live editor →

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.

Paste this in the live editor →

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.

Paste this in the live editor →

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.

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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.

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

AWS keywords

Matched left to right; the first keyword found as a whole word in the name wins.

KeywordsTypeIcon
lambda, functionLambdaλ
sqs, queueSQS
sns, notification, topicSNS📢
dynamodb, tableDynamoDB
s3, bucket, storageS3🥣
api, gateway, rest, httpAPI Gateway
kinesis, streamKinesis
eventbridge, eventsEventBridge
step, workflow, state machineStep Functions
ses, emailSES📧

User keywords

KeywordsTypeIcon
browser, web, desktopBrowser🌐
mobile, app, phone, ios, androidMobile📱

Note that user is not a User keyword. A bare User matches the AI list and renders as a Human.

AI keywords

KeywordsTypeIcon
agent, assistant, bot, coordinator, orchestrator, router, supervisor, researcher, writer, evaluator, critic, planner, executor, triage, rerankerAI Agent🤖
llm, model, gpt, claude, gemini, openai, anthropic, mistral, llama, generatorLLM🧠
tool, function call, action, plugin, mcp, api, search, retriever, scraperTool🔧
vectordb, vector, embedding, pinecone, chroma, qdrant, pgvector, weaviate, database, knowledge graphVector DB
memory, context, history, conversation, cacheMemory💾
guardrail, filter, safety, moderation, validatorGuardrail🛡
human, reviewer, person, operator, userHuman👤

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).

Paste this in the live editor →

=> 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).

Paste this in the live editor →

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.

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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).

Paste this in the live editor →

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.

Paste this in the live editor →

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.

Paste this in the live editor →

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

FreePro — $9/month
Diagrams saved to the cloud3100
Projects1Unlimited
The editor itselfNever cappedNever capped
ExportsAnimated WebM, WebP and GIF, still PNGAnimated WebM, WebP and GIF, still PNG
Service icons and syntaxAll of themAll 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.