GitLab workflow automation with glab CLI

Manage GitLab issues, MRs, epics, pipelines, and API calls using the glab CLI. Includes multi-host setup and safe message escaping.

Sby Skills Guide Bot
DevelopmentIntermediate
108/29/2026
Claude CodeCursorWindsurfCopilotCodex
#gitlab#glab-cli#workflow-automation#merge-requests#devops

Recommended for


name: glab description: GitLab workflow automation using the glab CLI. Use when the user asks to create, update, close, label, or comment on a GitLab issue, merge request, work item, or epic; to post a note, review comment, or discussion reply; to call the GitLab REST or GraphQL API through "glab api"; to inspect or retry pipelines and jobs; or to configure glab auth against gitlab.com, a self-managed instance, or a local GDK. Read it before any glab write operation, because several field flags fail silently - they exit 0 while posting the wrong content. version: 1.12.2 category: Development Workflow license: MIT metadata: audience: developers author: dgruzd workflow: gitlab

GitLab Workflow Skill

GitLab workflow management using glab CLI for merge requests, issues, and Git best practices.

Multiple GitLab Instances

glab auto-detects the GitLab host from your git remote. No GITLAB_HOST is needed when working inside a repository. For non-origin remotes (e.g. a local GDK instance added as a secondary remote), use glab config set remote_alias <remote>. Set GITLAB_HOST only when running outside a git repository or for a one-off command targeting a specific instance. See references/multi-host.md for non-origin remote setup and hostname derivation from remote URLs.

⚠️ Message Escaping — Common Trap

If your message contains backticks (`), $, or other shell special characters, NEVER inline them directly in -m "...". The shell interprets backticks as command substitution, silently mangling your message and producing errors like /bin/bash: line 1: client_name: command not found.

This has caused real production failures: agents posting malformed comments to GitLab MRs/issues, followed by apologetic correction notes.

❌ DON'T — inline backticks in double-quoted -m

# BROKEN: shell tries to execute `client_name` as a command
glab mr note 100 -m "Use `client_name` and `wor/` here." -R org/repo
# Error: /bin/bash: line 1: client_name: command not found
# The comment is posted as: "Use  and  here." (identifiers silently stripped)

# Also BROKEN: backslash-escaped backticks break in nested/scripted contexts
glab mr note 100 -m "Use \`client_name\`" -R org/repo
# Works in simple cases but fails when the command is double-quoted by a caller:
# bash -c "glab mr note 100 -m \"Use \`client_name\`\""  → still executes client_name

✅ DO — write to a file first, then pass via $(cat ...)

Pick a path appropriate for your environment (a mktemp result, a scoped workspace tmp file, whatever fits — the skill doesn't prescribe a specific path; agents choose one that's unique to their invocation to avoid clobbering parallel runs):

# MSG = path you choose (e.g. mktemp, ~/workspace/tmp/note-$$.md, etc.)
MSG=<agent picks>

cat > "$MSG" << 'EOF'
Use `client_name` and `wor/` here. The `glab` tool handles this.
EOF
glab mr note 100 -m "$(cat "$MSG")" -R org/repo

The single-quoted 'EOF' heredoc delimiter prevents ALL variable/backtick expansion when writing the file. The $(cat "$MSG") substitution is safe because the file content is already written literally. Triple-backtick code blocks (```) are also safe inside <<'EOF' heredocs — no escaping needed; only single-backticks and $ trigger command substitution.

✅ Also safe — glab api with -f flag

glab api --method POST "projects/org%2Frepo/merge_requests/100/notes" \
  -f "body=$(cat "$MSG")"

⚠️ Unquoted heredoc still interprets backticks

# BROKEN: unquoted EOF delimiter — backticks in body are still interpreted
cat > "$MSG" << EOF
Use `client_name` here.    # ← shell executes client_name when writing the file
EOF

⚠️ Heredoc-inside-heredoc breaks shell parsing

If your content itself contains a heredoc example with an unindented EOF terminator, the inner EOF at column 0 closes the outer heredoc early:

# BROKEN: the inner EOF is at column 0 — it closes the OUTER heredoc early
cat > "$OUTER" << 'EOF'
Here is the safe pattern:
cat > "$MSG" << 'EOF'
Use `client_name` here.
EOF
# ↑ This EOF terminates the OUTER heredoc — the lines below run as shell commands!
echo "more content..."
EOF
# ↑ This stray EOF becomes a command: "EOF: command not found"

Fix — use a different delimiter for the outer heredoc:

cat > "$OUTER" << 'OUTEREOF'
Here is the safe pattern:
  cat > "$MSG" << 'EOF'
  Use `client_name` here.
  EOF
OUTEREOF

Or write the file in chunks — first chunk uses >, subsequent chunks use >>, each with its own delimiter.

Rule of thumb: If the message contains `, $, !, or \ — write to a file with << 'EOF' first, always. If the content itself contains heredoc syntax, use a unique outer delimiter (e.g. OUTEREOF, MSGEOF) that won't appear in the body.

Creating Merge Requests

Always pass --push and -H <owner/repo>. Without --push, the branch may not exist on any remote yet. Without -H, glab may pick the wrong remote (e.g. a security mirror) as the source project, creating the MR from the wrong fork.

Second same-family remote? In a checkout with both origin (gitlab-org/gitlab) and a security mirror (gitlab-org/security/gitlab), glab mr create run non-interactively (no TTY, how agents run) silently picks the security fork as the head and fails with 400 {source_branch: [does not exist]}. Durable fix, set once per checkout: git config remote.origin.glab-resolved-head head. Per-command fallback for fresh checkouts: pass -H <owner/repo> (e.g. -H gitlab-org/gitlab). -R does not fix this — it pins only the base repo, not the head.

# Simple MR
glab mr create --push -H <owner/repo> --title "Add feature" --description "Brief description" --assignee <username>

# Complex MR - write description to file first (pick your own path)
glab mr create --push -H <owner/repo> --title "Add feature" --description "$(cat "$DESC")" --assignee <username>

Templates: Check .gitlab/merge_request_templates/ for project-specific templates.

Full flag list (Claude Code injects this automatically; other agents should run it):

!glab mr create --help

Updating Merge Requests

glab mr update <number> --description "$(cat "$DESC")"
glab mr view <number> -R <owner>/<repo>

Issue Management

The full, always-current flag list (Claude Code injects this automatically; other agents should run it):

!glab issue --help

view, note, list, create, update work as you'd expect. The non-obvious traps that --help won't warn you about:

# List is open by default and has NO --state flag — use --closed / --all instead
glab issue list --closed -R <owner>/<repo>
glab issue list --all    -R <owner>/<repo>

# Labels — use --label / --unlabel, NEVER +label or -label syntax
glab issue update 123 --label "new-label"
glab issue update 123 --unlabel "old-label"
# Scoped labels auto-replace within their scope — no --unlabel needed:
glab issue update 123 --label "status::doing"   # removes any existing status:: label

# Messages with backticks/$ — write to a file first (see Message Escaping above)
glab issue note <number> -m "$(cat "$MSG")" -R <owner>/<repo>

For issue state transitions (close/reopen via API) and posting notes via glab api: references/issue-api.md

Work Items

GitLab is migrating issues to work items. The URL shows /work_items/<iid> but the REST API is the same.

# ✅ Use the issues API — same IID, same endpoints
glab api --method GET "projects/org%2Fproject/issues/<iid>"

# ❌ /work_items/ REST endpoint does not exist
glab api --method GET "projects/org%2Fproject/work_items/<iid>"   # → 404

URL parsing: https://gitlab.com/org/project/-/work_items/539076glab api "projects/org%2Fproject/issues/539076"

Full details, GraphQL alternative, group-level work items, and the agent plan (Workplan) widget: references/work-items.md

MR Review

Since glab v1.94.0, glab mr note handles every common MR-comment shape (list, general, diff-line, reply, resolve/reopen) without raw glab api calls or hand-built position objects. Prefer it for any single-comment workflow. The MR IID is a positional argument (not --mr); omit it to auto-detect from the current branch.

# ✅ Use glab mr note for read/write/reply/resolve — single command per operation
glab mr note list 123 -F json                                       # read discussions
glab mr note create 123 -m "comment"                                # general
glab mr note create 123 --file main.go --line 42 -m "..."           # diff comment
glab mr note create 123 --reply abc12345 -m "..."                   # reply
glab mr note resolve 123 abc12345                                   # resolve thread

# ❌ Do NOT use glab api .../discussions for these operations
glab api --method GET "projects/<id>/merge_requests/123/discussions"             # use mr note list
glab api --method PUT ".../discussions/<id>" -f resolved=true       # use mr note resolve

Full flag list for the subcommands above (Claude Code injects this automatically; other agents should run it):

!glab mr note --help

Fall back to raw glab api .../draft_notes only for batched draft reviews (multiple inline comments published together via bulk_publish) — glab mr note has no draft mode.

Full reference (all flags, code suggestions, drafts/batch fallback, position objects): references/mr-review.md

Issue Links, Epics, and Nested Groups

MR Listing and Filtering

Full flag list (Claude Code injects this automatically; other agents should run it):

!glab mr list --help

Note: glab mr list lists open MRs by default and has no --state or --status flag — use --all, --merged, or --closed to change the state filter.

Search

For full search examples (instance / group / project, scope table, pagination): references/search.md

Quick reference:

glab api --method GET "search?scope=issues&search=<query>" | jq '.[] | {iid, title}'
glab api --method GET "groups/<group>/search?scope=merge_requests&search=<query>" | jq '.[]'
glab api --method GET "projects/<org>%2F<repo>/search?scope=issues&search=<query>" | jq '.[]'

Git and Commit Conventions

Follow the repo's own git conventions when present (a project may document them and enforce them with a commit linter). GitLab defaults:

git checkout -b feature/description   # feature branches
git checkout -b fix/description       # bug fixes
  • Capitalized, imperative commit subjects ("Add feature", not "Added feature" or "feat: add feature"); GitLab does not use conventional-commit subjects.
  • Reference issues/MRs with full URLs: Closes https://gitlab.com/org/project/-/issues/123.
  • Single-quote commit messages containing special characters: git commit -m 'Add note from https://gitlab.com/org/project/-/merge_requests/123'.

Agent Guidelines

The sections above cover the common workflows. These are the non-obvious traps and API quirks that are easy to get wrong and not discoverable from glab <cmd> --help:

  1. Read context firstglab issue view / glab mr view before implementing; check .gitlab/issue_templates/ and .gitlab/merge_request_templates/ for templates
  2. --jq works on subcommands, not on glab apiglab issue list/glab mr list/glab ci list accept a global --jq flag (filters JSON output); glab api does not — pipe its output through | jq '...' instead
  3. No --body flag — glab uses --description, not --body (which is a gh flag); they are not interchangeable
  4. Work items use the issues API/work_items/<iid> URLs → projects/.../issues/<iid>; the /work_items/ REST endpoint is a 404
  5. Epic comments need GraphQL — REST /notes GET+POST both → 404 (still true on GitLab 19.1); pass body/noteableId as GraphQL variables, never string-interpolate. See references/epic-comments.md
  6. No -R for group-level API-R expects OWNER/REPO; group endpoints use glab api "groups/..." directly
  7. Nested groups REST: %2Fgroups/org%2Fsubgroup/epics; unencoded slashes → 404
  8. GraphQL iid is a StringworkItem(iid: "16428") not workItem(iid: 16428)
  9. groups/<id>/work_items is 404 — use groups/<id>/epics (REST) or GraphQL
  10. project exposes workItems (plural), not workItem — under project use workItems(first: 1, iid: "IID") with no filter: argument; the singular workItem(iid:) field exists only under group/namespace
  11. Epic close/reopen via RESTstate_event=close/reopen on PUT groups/<id>/epics/<iid> works; no GraphQL needed
  12. Scoped labels auto-replace--label "status::doing" removes any existing status::* label; no --unlabel needed (this is a platform fact, true via the API generally, not just glab)
  13. Idempotent comments → --uniqueglab mr note create -m "..." --unique skips posting if an identical body already exists; matches on body only, so identical bodies on different diff lines still post
  14. Non-origin remotes → remote_alias — if origin points at one instance but you want glab to target another remote (e.g. a local GDK added as gdk), run glab config set remote_alias gdk rather than setting GITLAB_HOST per command. See references/multi-host.md
  15. Backticks in messages → write to a file first — never inline `/$ in -m "..." or --description "..."; write to a file you name (unique per invocation) using << 'EOF' (single-quoted delimiter, critical), then pass via $(cat "$FILE"). See the Message Escaping section above.
  16. Second same-family remote misroutes mr create — with a security mirror alongside origin, non-interactive mr create (no TTY) silently picks the fork as the head. Fix once per checkout: git config remote.origin.glab-resolved-head head; per-command fallback: -H <owner/repo> (-R does not fix it). See the "Creating Merge Requests" section.
  17. glab api note body: file → -F "body=@file"; prose with backticks/$ → write it literally to a file first. Inline text → -f; literal @here see above-f "body=@here see above" (never a file). Nested JSON → --input file -H "Content-Type: application/json". Repair: --method PUT -F "body=@file", not DELETE. Failures are silent (exit 0, HTTP 201); verify what landed with | jq '.body | length'. Details: references/mr-review.md.
  18. Always use full URLs in note/comment bodies (e.g. https://gitlab.com/org/project/-/issues/123) instead of short references (#123). This applies to issues, merge requests, epics, and so on. Short refs resolve against project context and render as literal text on group-level items (epics, group work items); full URLs expand everywhere.

Contributing Improvements

This skill is maintained in the GitLab monolith (gitlab-org/gitlab, under .claude/skills/glab/) and synced out to gitlab-org/ai/skills — the monolith copy is the source of truth. If you discover that any guidance here is inaccurate or outdated (e.g. a command that no longer works, a wrong flag, an incorrect API behavior), confirm with the user and open an MR against the monolith with the fix. Keep changes focused — one fix per MR.

Related skills