Skip to content

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:

  1. Header row: column names between pipes, | Component | Issue |.
  2. Delimiter row: one cell of hyphens per column, with optional colons: :--- left, :---: center, ---: right. Each cell needs at least one hyphen.
  3. Data rows: one line each. A row can be long, but it can't wrap onto a second line in the source.
  4. Blank line before and after the table, and escape any literal pipe as \|.
A merge request review table with GitLab references
Markdown
| 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   |
Preview
ComponentIssueMerge requestReviewerStatus
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.

Open in editor : A merge request review table with GitLab references

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.

Table methods in GitLab and when to use each
MethodSyntax or actionSortableBest for
Pipe table| a | b | + | --- | --- |Yes, by header click (text sort)Most tables: docs, MR descriptions, READMEs
json:table blockFenced code block tagged json:table, with items and fieldsPer field ("sortable": true)Generated data, filterable lists
Paste from a spreadsheetCopy cells in Excel, Google Sheets or Numbers, paste into the editorSame as pipe tableMoving existing data in fast
Rich text editorInsert table in the toolbar; rows, columns and merges from the cell menuNot in the editor itselfPeople who don’t want to type pipes
HTML <table><table>, <tr>, <td colspan="2">NoMerged 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 10 comes before 9 and Mar 2026 before Jan 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 with sortable columns, filter and captionMarkdown
```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"
}
```
json:table options
KeyTypeWhat it does
itemsArray of objectsOne object per row. Rows don’t need a value for every field.
fieldsArrayColumn order and labels: {"key": "p95", "label": "p95 latency"}. Omit it and the labels come from the first item’s keys.
sortableBoolean, per fieldAdds a sort control to that column only.
filterBooleantrue shows a text box that filters rows as you type.
markdownBooleantrue renders GitLab Markdown, including #123 and @user references, in items and the caption. Field labels stay plain text.
captionStringReplaces 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:

Minimal json:tableMarkdown
```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:

Cell content in GitLab pipe tables
ContentWorks?How
Bold, italic, strikethrough, inline code✅ Yes**bold**, _italic_, ~~old~~, backticks
Links and images✅ Yes[text](url), ![alt](image.png)
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❌ NoUse an HTML table with Markdown inside <td>
Merged cells❌ NoHTML colspan / rowspan
Multi-line cells with <br>
Markdown
| Service | Rollout steps                                        |
| :------ | :--------------------------------------------------- |
| API     | 1. Run `db:migrate`<br>2. Clear the CDN cache<br>3. Watch p99 latency |
| Worker  | Restart the queue consumers                          |
Preview
ServiceRollout steps
API1. Run db:migrate
2. Clear the CDN cache
3. Watch p99 latency
WorkerRestart the queue consumers

A numbered list typed as 1. …<br>2. … stays plain text with line breaks; it doesn't become a Markdown list.

Open in editor : Multi-line cells with <br>

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

Checkbox cells (GitLab 18.9+)
Markdown
| Check             | Done |
| :---------------- | :--: |
| Migrations run    | [x]  |
| Feature flag off  | [ ]  |
| Docs updated      | [~]  |
Preview
CheckDone
Migrations run
Feature flag off
Docs updated n/a

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:

Task list inside an HTML table cellHTML
<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 the api scope, and store it as a masked CI/CD variable, here MR_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_IID is set only there, so the job uses rules on $CI_PIPELINE_SOURCE == "merge_request_event".
.gitlab-ci.ymlYAML
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:

Write a table from shellShell
{
  echo "| Test suite | Passed | Failed | Duration |"
  echo "| :--------- | -----: | -----: | -------: |"
  echo "| unit       |    412 |      0 |    38 s  |"
  echo "| e2e        |     57 |      2 |   4m 10s |"
} > table.md

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

Table features: GitLab vs. GitHub
PlatformPipe tablesSortable headersJSON tablesCheckboxes in cellsPaste from spreadsheet
GitLab (GLFM)Issues, merge requests, comments, wikis, epics and repository Markdown files.YesYes: GA in 19.5; text sort onlyYes: json:table code blockYes: 18.9+, checkbox alone in the cellYes
GitHub (GFM)Issues, pull requests, comments, wikis and repository files.YesNoNoNo: [x] stays literal textYes

✅ 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

Broken: the | in the command adds a column
Markdown
| Command        | Purpose          |
| -------------- | ---------------- |
| `grep a | wc`  | Count matches    |
Preview
CommandPurposeColumn 3
`grep awc`Count matches

The row is cut at the pipe, so the command and its description land in the wrong cells.

Fixed: escape it as \|, even inside backticks
Markdown
| Command         | Purpose          |
| --------------- | ---------------- |
| `grep a \| wc` | Count matches    |
Preview
CommandPurpose
grep a | wcCount 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.

Before: blank cells
Markdown
| Region | Q1   | Q2   |
| ------ | ---: | ---: |
| EMEA   | 1.2M |      |
| APAC   |      | 0.8M |
Preview
RegionQ1Q2
EMEA1.2M
APAC0.8M
After: write N/A or None
Markdown
| Region | Q1   | Q2   |
| ------ | ---: | ---: |
| EMEA   | 1.2M | N/A  |
| APAC   | N/A  | 0.8M |
Preview
RegionQ1Q2
EMEA1.2MN/A
APACN/A0.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 colspan or rowspan (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).

Build a GitLab table in the generator

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.