GitLab Markdown Tables: Syntax, Sorting and JSON Tables
By MarkdownTables.com editorial teamUpdated
On this page
How do I create a table in GitLab?
GitLab Flavored Markdown (GLFM) builds on GitHub Flavored Markdown, so a table that renders on GitHub renders on GitLab. The rules are the ones in the Markdown table syntax guide:
- Header row: column names between pipes,
| Component | Issue |. - Delimiter row: one cell of hyphens per column, with optional colons:
:---left,:---:center,---:right. Each cell needs at least one hyphen. - Data rows: one line each. A row can be long, but it can't wrap onto a second line in the source.
- Blank line before and after the table, and escape any literal pipe as
\|.
| Component | Issue | Merge request | Reviewer | Status |
| :-------- | :----- | :------------ | :------------ | :----------- |
| Auth API | #1042 | !894 | @backend-lead | ✅ Approved |
| Database | #1088 | !902 | @dba-team | 🟡 In review |
| Webhooks | #1105 | !915 | @devops | ⏳ Pending || Component | Issue | Merge request | Reviewer | Status |
|---|---|---|---|---|
| Auth API | #1042 | !894 | @backend-lead | ✅ Approved |
| Database | #1088 | !902 | @dba-team | 🟡 In review |
| Webhooks | #1105 | !915 | @devops | ⏳ Pending |
On GitLab, #1042, !894 and @backend-lead become links to the issue, merge request and user. The preview here shows them as text.
The same table works in issue and merge request descriptions, comments, epics, wiki pages written in Markdown, and .md files in the repository. To build one without typing pipes, use the Markdown table generator, then check it with the table validator before you post.
Ways to make a table in GitLab
GitLab gives you five ways to end up with a table. Pick by where the data comes from and whether readers need to sort it.
| Method | Syntax or action | Sortable | Best for |
|---|---|---|---|
| Pipe table | | a | b | + | --- | --- | | Yes, by header click (text sort) | Most tables: docs, MR descriptions, READMEs |
json:table block | Fenced code block tagged json:table, with items and fields | Per field ("sortable": true) | Generated data, filterable lists |
| Paste from a spreadsheet | Copy cells in Excel, Google Sheets or Numbers, paste into the editor | Same as pipe table | Moving existing data in fast |
| Rich text editor | Insert table in the toolbar; rows, columns and merges from the cell menu | Not in the editor itself | People who don’t want to type pipes |
HTML <table> | <table>, <tr>, <td colspan="2"> | No | Merged cells, lists or task lists in cells |
Sortable tables: clickable column headers
According to the GitLab Markdown docs (checked October 2026), sortable tables are generally available from GitLab 19.5. You don't write any extra syntax: every rendered pipe table gets sort controls in its header cells.
- Click a header cell, or use its menu to choose Sort ascending, Sort descending or Reset sorting.
- Sorting is alphabetical and case-insensitive. Numbers and dates sort as text, so
10comes before9andMar 2026beforeJan 2026. - Empty cells always sort to the bottom. You sort by one column at a time.
- Tables with fewer than two data rows or more than 1,000 rows aren't sortable, and neither are JSON tables or tables in the rich text editor.
To make text sorting give the right order, write dates as ISO 2026-03-14 and pad numbers to the same width (009, 010) or keep units out of the value. If you are on GitLab Self-Managed or Dedicated below 19.5, headers aren't clickable; use a json:table block with sortable fields instead.
JSON tables (json:table)
A json:table block is a fenced code block that GitLab renders as an interactive table. It is the cleanest way to publish data your scripts already produce as JSON, and it can add a filter box.
```json:table
{
"fields": [
{"key": "service", "label": "Service", "sortable": true},
{"key": "p95", "label": "p95 latency", "sortable": true},
{"key": "owner", "label": "Owner"}
],
"items": [
{"service": "auth", "p95": "142 ms", "owner": "@team-identity"},
{"service": "billing", "p95": "318 ms", "owner": "@team-payments"},
{"service": "search", "p95": "97 ms", "owner": "@team-search"}
],
"filter": true,
"markdown": true,
"caption": "p95 latency by service, last 7 days"
}
```| Key | Type | What it does |
|---|---|---|
items | Array of objects | One object per row. Rows don’t need a value for every field. |
fields | Array | Column order and labels: {"key": "p95", "label": "p95 latency"}. Omit it and the labels come from the first item’s keys. |
sortable | Boolean, per field | Adds a sort control to that column only. |
filter | Boolean | true shows a text box that filters rows as you type. |
markdown | Boolean | true renders GitLab Markdown, including #123 and @user references, in items and the caption. Field labels stay plain text. |
caption | String | Replaces the default caption, “Generated with JSON data”. |
The shortest form is just an array of objects; the keys of the first object become the headers:
```json:table
[
{"version": "18.9", "change": "Checkboxes in table cells"},
{"version": "19.5", "change": "Sortable tables generally available"}
]
```Invalid JSON (a trailing comma, single quotes, unquoted keys) shows an error instead of a table, so build the block with a JSON serializer rather than by hand. If you need the same data as a pipe table for GitHub or a README, paste the array into the JSON to Markdown table converter.
What works inside a GitLab table cell
A cell holds one line of inline content. GitLab supports the same inline syntax as GitHub, plus its own references:
| Content | Works? | How |
|---|---|---|
| Bold, italic, strikethrough, inline code | ✅ Yes | **bold**, _italic_, ~~old~~, backticks |
| Links and images | ✅ Yes | [text](url),  |
| Line breaks | ✅ With HTML | <br> |
| Literal pipe | ✅ Escaped | \|, also inside code spans |
| GitLab references | ✅ Yes | #123, !456, @user, ~label, %milestone |
| Inline math | ✅ Yes | $x^2$; write \vert instead of | |
| Checkboxes | ✅ 18.9+ | [x], [ ] or [~] as the only content of the cell |
| Markdown lists, headings, code fences | ❌ No | Use an HTML table with Markdown inside <td> |
| Merged cells | ❌ No | HTML colspan / rowspan |
<br>| Service | Rollout steps |
| :------ | :--------------------------------------------------- |
| API | 1. Run `db:migrate`<br>2. Clear the CDN cache<br>3. Watch p99 latency |
| Worker | Restart the queue consumers || Service | Rollout steps |
|---|---|
| API | 1. Run db:migrate2. Clear the CDN cache 3. Watch p99 latency |
| Worker | Restart the queue consumers |
A numbered list typed as 1. …<br>2. … stays plain text with line breaks; it doesn't become a Markdown list.
Checkboxes and task lists in cells
From GitLab 18.9, a cell whose only content is [x], [ ] or [~] renders as a checkbox ([~] marks the item as not applicable). Add any other text to the cell and it stays literal. GitHub doesn't do this; there [x] in a cell is plain text (see checkboxes in tables).
| Check | Done |
| :---------------- | :--: |
| Migrations run | [x] |
| Feature flag off | [ ] |
| Docs updated | [~] || Check | Done |
|---|---|
| Migrations run | |
| Feature flag off | |
| Docs updated | |
Preview shows GitLab 18.9+ rendering. On GitHub and older GitLab versions the brackets show as text.
Open in editor : Checkbox cells (GitLab 18.9+)For several task items in one cell, or a task item next to other text, write the table in HTML and put a normal Markdown task list inside the cell. The blank lines after <td> and before </td> are what make GitLab parse the list:
<table>
<tr><th>Release step</th><th>Checklist</th></tr>
<tr>
<td>Deploy</td>
<td>
- [x] Run migrations
- [ ] Flip the feature flag
</td>
</tr>
</table>Issue, MR and user references
References are expanded inside cells exactly as in body text: #123 (issue), !456 (merge request), &789 (epic), @user, ~label and %milestone. Combined with sortable headers, a status table in an epic or MR description becomes a small dashboard that links to every item.
Rich text editor and spreadsheet paste
Paste from a spreadsheet. Copy a range of cells in Excel, Google Sheets or Apple Numbers and paste it into a GitLab Markdown field; GitLab inserts a Markdown table. Check the header row afterwards: the first copied row always becomes the header.
Rich text editor. Switch the comment or description box to the rich text editor and choose Insert table, then pick the dimensions. The chevron in the corner of each cell opens a menu to insert or delete rows and columns, and to merge or split selected cells. Unlike a pipe table, a cell there can hold paragraphs, lists or even another table. Pipe syntax can't express merged cells or block content, so a table that uses them ends up as HTML in the Markdown source, which is worth knowing before someone edits it in plain text.
Outside GitLab, the Excel to Markdown converter gives you the same result with alignment control, which helps for wiki pages you edit locally.
Post a Markdown table to a merge request from CI
A common pattern is a CI job that turns test, coverage or benchmark output into a Markdown table and posts it on the merge request. Three details decide whether it works:
- Use an access token, not
CI_JOB_TOKEN. The job token can reach only a limited set of API endpoints, and creating merge request notes generally isn't one of them. Create a project (or group) access token with theapiscope, and store it as a masked CI/CD variable, hereMR_NOTE_TOKEN. - Build the JSON with
jq. A Markdown table has newlines and often quotes; pasting it into a hand-written JSON string produces invalid JSON.jq -n --arg body …escapes it correctly. - Run only in merge request pipelines.
CI_MERGE_REQUEST_IIDis set only there, so the job usesruleson$CI_PIPELINE_SOURCE == "merge_request_event".
post_benchmark_table:
stage: test
image: alpine:3.20
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
script:
- apk add --no-cache curl jq python3
- python3 run_benchmarks.py --format markdown > table.md
- |
COMMENT_BODY="$(printf '### Benchmark results\n\n'; cat table.md)"
jq -n --arg body "$COMMENT_BODY" '{body: $body}' > note.json
curl --fail --silent --show-error \
--request POST \
--header "PRIVATE-TOKEN: $MR_NOTE_TOKEN" \
--header "Content-Type: application/json" \
--data @note.json \
"$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes"The endpoint is POST /projects/:id/merge_requests/:merge_request_iid/notes with a body parameter. If your script can't emit Markdown itself, a few echo lines are enough; right-align numeric columns so they compare at a glance:
{
echo "| Test suite | Passed | Failed | Duration |"
echo "| :--------- | -----: | -----: | -------: |"
echo "| unit | 412 | 0 | 38 s |"
echo "| e2e | 57 | 2 | 4m 10s |"
} > table.mdEach run adds a new comment. To keep one comment up to date instead, store the note ID from the first response and send PUT …/notes/:note_id on later runs. For Python-generated tables, see Markdown tables in Python.
GitLab vs. GitHub table support
Plain pipe tables are portable between the two platforms. The GitLab-only features don't degrade gracefully: a json:table block shows as a JSON code block on GitHub, and checkbox cells show as brackets.
| Platform | Pipe tables | Sortable headers | JSON tables | Checkboxes in cells | Paste from spreadsheet |
|---|---|---|---|---|---|
| GitLab (GLFM)Issues, merge requests, comments, wikis, epics and repository Markdown files. | Yes | Yes: GA in 19.5; text sort only | Yes: json:table code block | Yes: 18.9+, checkbox alone in the cell | Yes |
| GitHub (GFM)Issues, pull requests, comments, wikis and repository files. | Yes | No | No | No: [x] stays literal text | Yes |
✅ supported · ⚠️ partly or with a workaround · ❌ not supported · Last verified October 2026
Both render HTML tables with colspan and rowspan. GitLab Self-Managed instances get features when they upgrade, so check your version.
If a repository is mirrored to both, stick to pipe tables and put GitLab-specific blocks only in issues and MRs. For GitHub specifics, see GitHub README tables. Bitbucket Cloud also renders pipe tables in Markdown files and pull requests, but its documented HTML support is narrower, so preview anything beyond plain pipe syntax there.
Common mistakes and fixes
A pipe inside a code span splits the cell
| in the command adds a column| Command | Purpose |
| -------------- | ---------------- |
| `grep a | wc` | Count matches || Command | Purpose | Column 3 |
|---|---|---|
| `grep a | wc` | Count matches |
The row is cut at the pipe, so the command and its description land in the wrong cells.
\|, even inside backticks| Command | Purpose |
| --------------- | ---------------- |
| `grep a \| wc` | Count matches || Command | Purpose |
|---|---|
grep a | wc | Count matches |
Empty cells
GitLab's documentation asks you to avoid empty cells for accessibility: a screen reader announces nothing, and readers can't tell “zero” from “unknown”. Empty cells also always sort to the bottom.
| Region | Q1 | Q2 |
| ------ | ---: | ---: |
| EMEA | 1.2M | |
| APAC | | 0.8M || Region | Q1 | Q2 |
|---|---|---|
| EMEA | 1.2M | |
| APAC | 0.8M |
| Region | Q1 | Q2 |
| ------ | ---: | ---: |
| EMEA | 1.2M | N/A |
| APAC | N/A | 0.8M || Region | Q1 | Q2 |
|---|---|---|
| EMEA | 1.2M | N/A |
| APAC | N/A | 0.8M |
Other mistakes
- No blank line before the table after a list or paragraph. Add one; it also satisfies markdownlint.
- Different cell counts in the header and delimiter rows stop the table rendering at all. The troubleshooting guide covers the rest.
- Header alignment looks different per browser. GitLab's docs note that headers are left-aligned in Chrome and Firefox but centered in Safari unless you set alignment. Add colons to the delimiter row to make it consistent.
- Expecting numeric sort. Header sorting is text-only; see the formatting tips above.
When to switch to an HTML table
Switch from pipe syntax to an HTML table when you need:
- merged cells with
colspanorrowspan(merged cells) - Markdown lists, several task items or code blocks inside a cell
- row headers or a real
<caption>
Switch to json:table when the data is generated, longer than a screen, or needs a filter box. Keep large tables readable in MRs by wrapping them in <details> (see collapsible tables).
Frequently asked questions
How do I make a table in GitLab Markdown?
Write a header row between pipes, a delimiter row of hyphens (| --- | --- |), then one line per row: | Name | Owner |. Colons in the delimiter row set alignment. It is the same pipe syntax GitHub uses, and it works in issues, merge requests, comments, wikis and .md files.
Can GitLab Markdown tables be sorted?
Yes. As of the GitLab docs for 19.5, rendered tables get sort controls in each header cell: click the header or choose Sort ascending, Sort descending or Reset sorting. Values sort as case-insensitive text, so numbers and dates do not sort numerically. For per-column sorting you control, use a json:table block with "sortable": true fields.
What is a json:table in GitLab?
A fenced code block with the language json:table that GitLab renders as a table. It takes items (one object per row) and optional fields (column keys, labels and sortable), filter: true for a search box, markdown: true to render GitLab Markdown in cells, and a caption.
Can I put a checkbox in a GitLab table cell?
Yes, in GitLab 18.9 and later, when the checkbox is the only content of the cell: [x], [ ] or [~] (not applicable). For several task items in one cell, use an HTML <table> with a Markdown task list inside the <td>, separated by blank lines.
How do I add a line break inside a GitLab table cell?
Use <br>, for example | Step 1<br>Step 2 |. A row has to stay on one line in the source, and Markdown lists do not work in pipe-table cells. For real lists, write the table in HTML and leave blank lines around the Markdown inside each <td>.
Can CI_JOB_TOKEN post a merge request comment?
Generally no. The job token can call only a limited set of API endpoints, and creating merge request notes is not one of them. Store a project or group access token with the api scope as a masked CI/CD variable and send it in the PRIVATE-TOKEN header.
Can I merge cells in a GitLab Markdown table?
Not in pipe syntax, which has no colspan or rowspan. Write the table in HTML with colspan or rowspan, which GitLab renders, or select cells in the rich text editor and choose Merge from the cell menu (it stores the table as HTML). See merged cells.
More platform guides: GitHub, VS Code and Obsidian and Notion.