Matrices
A matrix puts rows against columns and gives every cell a meaning. Use it to compare options against criteria, to score them with weights and add up a total, to say who does what in a release with a RACI chart (Responsible, Accountable, Consulted, Informed), or to place risks on a grid of likelihood against impact. Today most agents write a Markdown table for these, which GitHub draws flat and which cannot say a cell is a yes, a score, or a role. A matrix draws each cell as a mark: a green tick, a row of dots, a lettered disc, or a heat map cell.
TL;DR
| Question | Answer |
|---|---|
| What is it for? | A table where the cells mean something: options against criteria, a weighted decision, a RACI chart, or a risk heat map. |
| How do I start one? | Write matrix "Title", a columns line, and one row line for each row. Add style=score, style=raci, or style=risk for the other three jobs. |
| What goes in a cell? | yes, no, partial, na, a score from 0 to 5, or quoted text. In a RACI chart the letters R, A, C, and I. A legend line says what each mark means. |
| How do I pick a winner? | Write recommend "Row name". The row gets a tone wash, a stronger outline, and a Recommended tab. It is never chosen from the totals for you. |
| What does Mermaid convert? | Nothing. Mermaid has no matrix type. See From Mermaid for what to do with a Markdown table. |
| How big can it be? | At most 12 columns, 40 rows, and 60 items on a grid. Warnings start at 20 rows and 30 items, and when the picture is wider than a Markdown column. |
| How is it drawn? | A rounded frame with the title in a corner tab, a header band, light row bands, an icon chip beside each row name, a mark for each cell, and a legend for a RACI chart or a risk grid. |
Ask your agent
Use Plotwork to draw a matrix comparing Kafka, RabbitMQ, Amazon SQS and NATS as message brokers. Use throughput and ease of ops as scores, ordering and a managed offer as yes, no or partial, and the typical cost as text. Recommend Kafka.Use Plotwork to draw a decision matrix for choosing a database. Score PostgreSQL, MySQL, MongoDB, DynamoDB and SQLite from 0 to 5 on query power, scale, ops effort and ecosystem, weight query power three times and scale and ops effort twice, and recommend the highest total.Use Plotwork to draw a permission matrix for the Orders API. The columns are the roles guest, customer, support, finance and admin, each with an icon. The rows are the endpoints, such as GET /orders/:id, with what each does under it. Mark each cell allowed, denied, or partial, and add a legend that says partial means own records only.Use Plotwork to draw a RACI chart for shipping release 2.4. The roles are engineering lead, QA, product, support and ops, and the tasks are freezing the scope, cutting the branch, running the regression tests, writing the release notes, deploying and telling customers. Give every task one Accountable role.Use Plotwork to draw a risk heat map for the launch. Place eight risks by likelihood and impact, with an icon on the ones that need a name to stand out.Example
Four message brokers compared on five criteria. Throughput and ease of ops are scores, ordering and the managed offer are answers, and the cost is text. The recommended row has a tone wash, a stronger outline, and a tab that says so.
matrix "Message broker options" desc="Four message brokers compared on five criteria. Kafka is the pick for the event pipeline because it keeps order at the highest throughput."
columns "Throughput" "Ordering" "Ease of ops" "Managed offer" "Typical cost"
row "Kafka" icon=simple:apachekafka sub="Log-based streaming" : 5, yes, 2, partial, "Medium"row "RabbitMQ" icon=simple:rabbitmq sub="Classic queues" : 3, yes, 3, partial, "Low"row "Amazon SQS" icon=aws:sqs sub="Managed queues" : 4, no, 5, yes, "Pay per use"row "NATS" icon=simple:nats sub="Light messaging" : 4, partial, 4, no, "Low"
recommend "Kafka"legend partial="Limited"Columns: Throughput, Ordering, Ease of ops, Managed offer, Typical cost.
- Kafka (Log-based streaming, recommended): Throughput: 5 of 5; Ordering: yes; Ease of ops: 2 of 5; Managed offer: Limited; Typical cost: Medium.
- RabbitMQ (Classic queues): Throughput: 3 of 5; Ordering: yes; Ease of ops: 3 of 5; Managed offer: Limited; Typical cost: Low.
- Amazon SQS (Managed queues): Throughput: 4 of 5; Ordering: no; Ease of ops: 5 of 5; Managed offer: yes; Typical cost: Pay per use.
- NATS (Light messaging): Throughput: 4 of 5; Ordering: Limited; Ease of ops: 4 of 5; Managed offer: no; Typical cost: Low.
The language
One statement to a line. Text is always in double quotes, options are key=value, and # starts a comment. A cell is not an option, so the cells of a row follow a colon and are separated by commas.
| Statement | Meaning | Example |
|---|---|---|
matrix "Title" style=<style> total=<on or off> desc="<text>" | The header. All three options are optional, and so is the title. | matrix "Database decision" style=score |
columns "<Name>" weight=<1 to 9> icon=<icon> "<Name>" ... | The columns. Each quoted name may be followed by its own weight= and icon=. Several columns lines add up. | columns "Throughput" weight=3 icon=lucide:gauge "Cost" |
row "<Name>" icon=<icon> tone=<tone> sub="<text>" : <cell>, <cell> | A row, with one cell for each column after the colon. sub= is a quiet second line under the name. | row "Kafka" icon=simple:apachekafka : 5, yes, "Medium" |
recommend "<Row name>" | Marks one row as the pick. | recommend "Kafka" |
legend yes="<text>" partial="<text>" no="<text>" na="<text>" | What each mark means, drawn under the table. Only the marks named are shown, in the order written. Not in a RACI chart or a risk grid, which have their own. | legend yes="Allowed" no="Denied" |
item "<Label>" likelihood=<1 to 5> impact=<1 to 5> icon=<icon> | A risk on the grid of style=risk. | item "Data loss" likelihood=2 impact=5 |
levels <likelihood or impact> "<L1>" "<L2>" "<L3>" "<L4>" "<L5>" title="<text>" | The names of the five levels of one axis of a risk grid, lowest first, and optionally the axis. | levels impact "Minor" "Moderate" "Major" "Severe" "Critical" |
desc "<text>" | The plain-language description. The same as desc= on the header. | desc "Four brokers compared." |
The four styles decide which statements and which cells are valid. A statement that belongs to another style is refused with a hint that says where it goes.
| Style | For | Cells |
|---|---|---|
compare | Options against criteria, and feature matrices. This is the default. | yes, no, partial, na, a score, or text |
score | A decision matrix. The weighted total column is on. | The same, but scores are what count |
raci | Who is Responsible, Accountable, Consulted, or Informed for each task. | R, A, C, I, a pair such as "A R", or na |
risk | A heat map of likelihood against impact. | No rows: items go on a grid |
Cells
Each kind of cell has its own mark, so the table reads at a glance and still makes sense in grayscale.
| You write | You get |
|---|---|
yes | A green disc with a tick. |
no | A red disc with a cross. |
partial | An amber disc with a half-filled ring. |
na | A short quiet dash: the question does not apply. |
4 | Five dots with four filled, in the tone of the row. A score is a whole number from 0 to 5. |
"Medium" | The text, in the quiet ink, centred in its column. A long text wraps to a second line before it is cut. |
A score above 5 is refused, and for a score out of 10 the hint says what to write. A number that is not whole is refused too, so a dot is never half drawn.
A half disc means something different in every table, so say what it means with a legend line: legend partial="Own records only". The legend draws the same discs under the table with their words, and the text description reads each named mark as its words. When a cell is partial and no legend names it, lint warns.
A permission matrix
Roles are the columns and endpoints are the rows, so a reader finds who may call what at a glance. Give each role an icon, put what the endpoint does in sub=, and name the marks in a legend. Quote a route such as "GET /orders/:id": a slash is only allowed inside quotes.
matrix "Orders API permissions" desc="Who may call each endpoint of the Orders API. Customers reach their own orders, invoices, and account only; support refunds within a limit; only admins change settings."
columns "Guest" icon=lucide:user "Customer" icon=lucide:user-check "Support" icon=lucide:message-circle "Finance" icon=lucide:wallet "Admin" icon=lucide:shield-check
row "GET /products" icon=lucide:package sub="Browse the catalogue" : yes, yes, yes, yes, yesrow "POST /orders" icon=lucide:credit-card sub="Place an order" : no, yes, no, no, yesrow "GET /orders/:id" icon=lucide:receipt sub="Read one order" : no, partial, yes, yes, yesrow "PATCH /orders/:id" icon=lucide:pen-line sub="Change an open order" : no, partial, yes, no, yesrow "POST /orders/:id/refund" icon=lucide:undo-2 sub="Refund an order" : no, no, partial, yes, yesrow "GET /invoices" icon=lucide:file-text sub="List invoices" : no, partial, no, yes, yesrow "GET /users" icon=lucide:users sub="List accounts" : no, no, yes, no, yesrow "DELETE /users/:id" icon=lucide:user-x sub="Close an account" : no, partial, no, no, yesrow "GET /audit-log" icon=lucide:scroll-text sub="Read the audit trail" : no, no, no, yes, yesrow "PUT /settings" icon=lucide:settings sub="Change platform settings" : no, no, no, no, yes
legend yes="Allowed" partial="Own records only, or within a limit" no="Denied"Columns: Guest, Customer, Support, Finance, Admin.
- GET /products (Browse the catalogue): Guest: Allowed; Customer: Allowed; Support: Allowed; Finance: Allowed; Admin: Allowed.
- POST /orders (Place an order): Guest: Denied; Customer: Allowed; Support: Denied; Finance: Denied; Admin: Allowed.
- GET /orders/:id (Read one order): Guest: Denied; Customer: Own records only, or within a limit; Support: Allowed; Finance: Allowed; Admin: Allowed.
- PATCH /orders/:id (Change an open order): Guest: Denied; Customer: Own records only, or within a limit; Support: Allowed; Finance: Denied; Admin: Allowed.
- POST /orders/:id/refund (Refund an order): Guest: Denied; Customer: Denied; Support: Own records only, or within a limit; Finance: Allowed; Admin: Allowed.
- GET /invoices (List invoices): Guest: Denied; Customer: Own records only, or within a limit; Support: Denied; Finance: Allowed; Admin: Allowed.
- GET /users (List accounts): Guest: Denied; Customer: Denied; Support: Allowed; Finance: Denied; Admin: Allowed.
- DELETE /users/:id (Close an account): Guest: Denied; Customer: Own records only, or within a limit; Support: Denied; Finance: Denied; Admin: Allowed.
- GET /audit-log (Read the audit trail): Guest: Denied; Customer: Denied; Support: Denied; Finance: Allowed; Admin: Allowed.
- PUT /settings (Change platform settings): Guest: Denied; Customer: Denied; Support: Denied; Finance: Denied; Admin: Allowed.
Weights and totals
Write weight=3 on a column to make it count three times as much in the total, and total=on on the header, or style=score, to add the column. The total of a row is the sum of each score times the weight of its column, drawn as a number with a bar for its share of the most it could be. Only scores add up: a column of yes and no answers or text leaves the total alone, and a cell that is not a score in a column of scores adds nothing, so lint says so.
Each weight is a small pill under its title, so a reader sees why a column counts for more. The total is also in the text description, so an agent can check the sum.
matrix "Database decision" style=score desc="Five databases scored from 0 to 5 on four weighted criteria. PostgreSQL has the highest total."
columns "Query power" weight=3 icon=lucide:search "Scale" weight=2 icon=lucide:layers "Ops effort" weight=2 icon=lucide:settings "Ecosystem"
row "PostgreSQL" icon=simple:postgresql : 5, 4, 4, 5row "MySQL" icon=simple:mysql : 4, 4, 4, 5row "MongoDB" icon=simple:mongodb : 3, 5, 3, 4row "DynamoDB" icon=aws:dynamodb : 2, 5, 5, 3row "SQLite" icon=simple:sqlite : 3, 1, 5, 4
recommend "PostgreSQL"Columns: Query power (weight 3), Scale (weight 2), Ops effort (weight 2), Ecosystem.
- PostgreSQL (recommended): Query power: 5 of 5; Scale: 4 of 5; Ops effort: 4 of 5; Ecosystem: 5 of 5; Total: 36 of 40.
- MySQL: Query power: 4 of 5; Scale: 4 of 5; Ops effort: 4 of 5; Ecosystem: 5 of 5; Total: 33 of 40.
- MongoDB: Query power: 3 of 5; Scale: 5 of 5; Ops effort: 3 of 5; Ecosystem: 4 of 5; Total: 29 of 40.
- DynamoDB: Query power: 2 of 5; Scale: 5 of 5; Ops effort: 5 of 5; Ecosystem: 3 of 5; Total: 29 of 40.
- SQLite: Query power: 3 of 5; Scale: 1 of 5; Ops effort: 5 of 5; Ecosystem: 4 of 5; Total: 25 of 40.
Highest total: PostgreSQL with 36 of 40.
Recommending a row
recommend "Kafka" names one row, and a matrix recommends one row. The row is lifted into a card with a wash of its tone (green when the row has no tone=), a stronger outline, and a Recommended tab in its corner, drawn the way a group's name is in a flow diagram. The matrix never picks the winner for you: when the recommended row has a lower total than another, lint warns and suggests saying why in desc=.
A RACI chart
With style=raci the columns are roles and the rows are tasks. Each letter is a disc, and the stronger the role the more solid the disc: Accountable is filled, Responsible is a wash with a border, Consulted is a plain ring, and Informed is a dashed ring. A legend of the four letters sits under the table. A cell may name several roles, written as "A R", and they draw side by side with the strongest first.
matrix "Release 2.4 roles" style=raci desc="Who does what to ship release 2.4. Every task has one Accountable role and at least one Responsible role."
columns "Eng lead" "QA" "Product" "Support" "Ops"
row "Freeze the scope" : C, I, "A R", I, narow "Cut the release branch" : "A R", I, I, na, Crow "Run the regression tests" : A, R, I, na, narow "Write the release notes" : C, I, "A R", C, narow "Deploy to production" : A, C, I, I, Rrow "Tell customers" : I, na, A, R, naRoles: Eng lead, QA, Product, Support, Ops.
- Freeze the scope: Eng lead: Consulted; QA: Informed; Product: Accountable and Responsible; Support: Informed; Ops: not involved.
- Cut the release branch: Eng lead: Accountable and Responsible; QA: Informed; Product: Informed; Support: not involved; Ops: Consulted.
- Run the regression tests: Eng lead: Accountable; QA: Responsible; Product: Informed; Support: not involved; Ops: not involved.
- Write the release notes: Eng lead: Consulted; QA: Informed; Product: Accountable and Responsible; Support: Consulted; Ops: not involved.
- Deploy to production: Eng lead: Accountable; QA: Consulted; Product: Informed; Support: Informed; Ops: Responsible.
- Tell customers: Eng lead: Informed; QA: not involved; Product: Accountable; Support: Responsible; Ops: not involved.
A risk heat map
With style=risk there are no rows or columns. Each item is placed by its likelihood and impact, both from 1 to 5, in a grid of 25 cells. A cell takes a band from likelihood times impact: low for 1 to 4, moderate for 5 to 9, high for 10 to 16, and critical for 17 to 25, shaded from green through amber and orange to red, with the bands named in a legend. The first row is the highest impact and the first column the lowest likelihood, so the worst risks are at the top right. Several items in one cell stack as chips, and the row grows to hold them. Name the levels with levels when the defaults do not fit: they run from Rare to Almost certain and from Negligible to Severe.
matrix "Launch risks" style=risk desc="Eight risks to the launch placed by how likely they are and how much damage they would do. The red cells need an owner this week."
item "Data loss on migration" likelihood=2 impact=5 icon=lucide:databaseitem "Cloud region outage" likelihood=3 impact=4 icon=lucide:clouditem "Key engineer leaves" likelihood=3 impact=3 icon=lucide:user-xitem "Scope creep" likelihood=5 impact=2item "Security audit fails" likelihood=2 impact=4 icon=lucide:shielditem "Budget overrun" likelihood=4 impact=3item "Supplier delivers late" likelihood=4 impact=4 icon=lucide:packageitem "Minor browser bugs" likelihood=5 impact=1Likelihood runs from Rare (1) to Almost certain (5); Impact runs from Negligible (1) to Severe (5).
High (score 10 to 16):
- Data loss on migration: Likelihood 2 (Unlikely), Impact 5 (Severe), score 10
- Cloud region outage: Likelihood 3 (Possible), Impact 4 (Major), score 12
- Scope creep: Likelihood 5 (Almost certain), Impact 2 (Minor), score 10
- Budget overrun: Likelihood 4 (Likely), Impact 3 (Moderate), score 12
- Supplier delivers late: Likelihood 4 (Likely), Impact 4 (Major), score 16
Moderate (score 5 to 9):
- Key engineer leaves: Likelihood 3 (Possible), Impact 3 (Moderate), score 9
- Security audit fails: Likelihood 2 (Unlikely), Impact 4 (Major), score 8
- Minor browser bugs: Likelihood 5 (Almost certain), Impact 1 (Negligible), score 5
Options and colour
| Option | On | Values | Default |
|---|---|---|---|
style= | header | compare, score, raci, risk | compare |
total= | header | on or off, for compare and score | on for score, else off |
weight= | column | a whole number from 1 to 9 | 1 |
icon= | column, row, item | any bundled icon, or none | none |
tone= | row | any tone | neutral, blue in a RACI chart, green for the recommended row |
sub= | row | quoted text, one line | none |
title= | levels | quoted text, the name of the axis | Likelihood and Impact |
The theme owns every color. A tone sets the dots, the bar, the icon chip, and the recommended card of a row, and a RACI chart draws every role in the tone of its row, so a tone= on a row marks something else, such as which team owns the task. Nobody writes a color value, so a matrix has real contrast in light and in dark.
Limits and warnings
| Limit | Value | What happens past it |
|---|---|---|
| Columns | 12 | E140 before any layout, with the advice to keep the criteria that decide something. |
| Rows | 40 | E140, with the advice to split by theme or team. |
| Items on a grid | 60 | E140. |
| Rows before the density warning | 20 | W200. |
| Items before the density warning | 30 | W200. |
| Cell text | 144px preferred, 208px hard, 2 lines | Text wraps at the preferred width, a long word widens the column up to the hard width, and only past that is it cut with an ellipsis and warning W203. |
| A row name | 176px preferred, 264px hard, 2 lines | The same, with a second line cut at one line. |
Every mistake comes back as a result with a code, a line, a column, and a hint. Warnings never stop the drawing.
| Code | When it fires | Fix |
|---|---|---|
E100 | A row with too few or too many cells, a missing colon, a missing row name, an empty name or cell, a RACI cell that is not a role, a statement from the other style, or a matrix with no rows. | Read the hint, which names the cells to write. A row with too many cells is flagged at the first extra one. |
E110, E101, E121, E122, E135 | The shared codes: an unknown statement, an unterminated string, an unknown tone or icon, and an unknown option. | Read the hint. |
E130 | A column, a row, an item, or an axis named twice, a second recommend or legend, or a mark named twice in a legend. | Say it once, or give each its own name. |
E131 | recommend names a row that does not exist. | Use one of the row names, as the hint says. |
E136 | A style that is none of the four, a total that is not on or off, a weight outside 1 to 9, a score outside 0 to 5 or not whole, a level outside 1 to 5, or a cell word that is no value. | Use the value the hint offers. A score out of 10 is told what to write. |
E140 | More than 12 columns, 40 rows, or 60 items. | Split the matrix. |
W200 | More than 20 rows or 30 items, a RACI task with no Accountable, more than one Accountable, or no Responsible role, a weight that counts for nothing, a total with no scores to add, a cell that is not a score in a column of scores, a recommended row below the highest total, more than four risks in one cell, or a partial cell with no legend that says what partial means. | Follow the hint in the warning. |
W202 | The picture is wider than a Markdown column and its text would shrink. | Remove or merge columns, shorten the names, or split the matrix. About eight columns fit. |
W203 | Text was cut with an ellipsis. | Shorten the name, the cell, or the level. |
From Mermaid
Mermaid has no matrix type, so there is nothing for convert_mermaid to convert, and a Mermaid diagram that tries to be a table is better redrawn. A Markdown table is the usual source, and an agent can turn it into a matrix in one step: the header row becomes the columns line, each following row becomes a row line, and the cells become values.
Use Plotwork to turn this Markdown table into a matrix: write the header cells as columns, write each row as a row, turn a tick or the word yes into yes, a cross or no into no, and anything in between into partial, keep a number as a score from 0 to 5, and keep any other cell as quoted text.| In the table | In the matrix |
|---|---|
| The header row | columns "A" "B" "C" |
| The first cell of a row | The name in row "Name" : |
| A tick, a cross, or yes and no | yes or no |
| A rating such as 4 out of 5 | A score of 4 |
| A short note or a price | Quoted text such as "Medium" |
| A cell with nothing to say | na |
If the table is a priority list with two scales, such as effort against impact, the quadrant chart places each item on both and reads the zone it falls in.
See also
| Doc | Why |
|---|---|
| Writing diagrams | The flow, sequence and schema languages, and the rules every kind shares |
| Examples to copy | Finished diagrams with the prompt that gets an agent to write each one |
| Quadrant charts | Items placed on two axes and read by the zone they fall in |
| Treemaps | Sizes inside a hierarchy, when the area carries the meaning |
| Migrating from Mermaid | Convert existing Mermaid diagrams, and what is dropped |
| Language reference | Every kind, node type, option and diagnostic |
| Diagram kinds | The kinds Plotwork draws and the header of each |