# Matrices

> Page: https://docs.plotwork.dev/matrices. Index of every page: https://docs.plotwork.dev/llms.txt

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](#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

```text
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.
```

```text
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.
```

```text
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.
```

```text
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.
```

```text
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.

**Diagram source**

```diagram
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"
```

*Picture: Message broker options: 4 rows compared on 5 columns. Recommended: Kafka. Four message brokers compared on five criteria. Kafka is the pick for the event pipeline because it keeps order at the highest throughput.*

**Text description**

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

**Diagram source**

```diagram
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, yes
row "POST /orders" icon=lucide:credit-card sub="Place an order" : no, yes, no, no, yes
row "GET /orders/:id" icon=lucide:receipt sub="Read one order" : no, partial, yes, yes, yes
row "PATCH /orders/:id" icon=lucide:pen-line sub="Change an open order" : no, partial, yes, no, yes
row "POST /orders/:id/refund" icon=lucide:undo-2 sub="Refund an order" : no, no, partial, yes, yes
row "GET /invoices" icon=lucide:file-text sub="List invoices" : no, partial, no, yes, yes
row "GET /users" icon=lucide:users sub="List accounts" : no, no, yes, no, yes
row "DELETE /users/:id" icon=lucide:user-x sub="Close an account" : no, partial, no, no, yes
row "GET /audit-log" icon=lucide:scroll-text sub="Read the audit trail" : no, no, no, yes, yes
row "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"
```

*Picture: Orders API permissions: 10 rows compared on 5 columns. 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.*

**Text description**

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

**Diagram source**

```diagram
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, 5
row "MySQL" icon=simple:mysql : 4, 4, 4, 5
row "MongoDB" icon=simple:mongodb : 3, 5, 3, 4
row "DynamoDB" icon=aws:dynamodb : 2, 5, 5, 3
row "SQLite" icon=simple:sqlite : 3, 1, 5, 4

recommend "PostgreSQL"
```

*Picture: Database decision: 5 rows scored on 4 columns. Recommended: PostgreSQL. Five databases scored from 0 to 5 on four weighted criteria. PostgreSQL has the highest total.*

**Text description**

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

**Diagram source**

```diagram
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, na
row "Cut the release branch" : "A R", I, I, na, C
row "Run the regression tests" : A, R, I, na, na
row "Write the release notes" : C, I, "A R", C, na
row "Deploy to production" : A, C, I, I, R
row "Tell customers" : I, na, A, R, na
```

*Picture: Release 2.4 roles: 6 tasks across 5 roles. Who does what to ship release 2.4. Every task has one Accountable role and at least one Responsible role.*

**Text description**

**Roles: 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.

**Diagram source**

```diagram
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:database
item "Cloud region outage" likelihood=3 impact=4 icon=lucide:cloud
item "Key engineer leaves" likelihood=3 impact=3 icon=lucide:user-x
item "Scope creep" likelihood=5 impact=2
item "Security audit fails" likelihood=2 impact=4 icon=lucide:shield
item "Budget overrun" likelihood=4 impact=3
item "Supplier delivers late" likelihood=4 impact=4 icon=lucide:package
item "Minor browser bugs" likelihood=5 impact=1
```

*Picture: Launch risks: 8 risks placed by Likelihood and Impact on a 5 by 5 grid. 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.*

**Text description**

**Likelihood 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.

```text
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](https://docs.plotwork.dev/quadrant-charts.md) places each item on both and reads the zone it falls in.

## See also

| Doc | Why |
|---|---|
| [Writing diagrams](https://docs.plotwork.dev/writing-diagrams.md) | The flow, sequence and schema languages, and the rules every kind shares |
| [Examples to copy](https://docs.plotwork.dev/examples.md) | Finished diagrams with the prompt that gets an agent to write each one |
| [Quadrant charts](https://docs.plotwork.dev/quadrant-charts.md) | Items placed on two axes and read by the zone they fall in |
| [Treemaps](https://docs.plotwork.dev/treemaps.md) | Sizes inside a hierarchy, when the area carries the meaning |
| [Migrating from Mermaid](https://docs.plotwork.dev/migrating-from-mermaid.md) | Convert existing Mermaid diagrams, and what is dropped |
| [Language reference](https://docs.plotwork.dev/reference/language.md) | Every kind, node type, option and diagnostic |
| [Diagram kinds](https://docs.plotwork.dev/reference/language/kinds.md) | The kinds Plotwork draws and the header of each |
