Skip to content
Back to skills

Write Ticket Description

ASecurity

Voice guide for writing issue/ticket/task/epic descriptions. Use this skill for ALL issue creation — GitHub issues, Jira tasks, Monday tasks, Linear tasks, and epics. Contains section structure, voice rules, and anti-patterns. Never write ticket descriptions without invoking this skill first.

  • 24 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 3, 2026
developmentgobashsqlgitapidatabaseperformance

Works with

  • cli
  • api
  • mcp

Security analysis

A100/100

Scanned September 27, 2026

npx -y skills add ooloth/dotfiles --skill write-ticket-description --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Write Ticket Description?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Write Ticket Description
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/ooloth-write-ticket-description/badge)](https://www.skillsdirectory.com/skills/ooloth-write-ticket-description)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: write-ticket-description
description: Voice guide for writing issue/ticket/task/epic descriptions. Use this skill for ALL issue creation — GitHub issues, Jira tasks, Monday tasks, Linear tasks, and epics. Contains section structure, voice rules, and anti-patterns. Never write ticket descriptions without invoking this skill first.
allowed-tools: [Bash, Read, Glob, Grep]
---

# Writing Ticket Descriptions

## ⚡ QUICK START

1. **Duplicate check** — search the platform for issues with similar titles; read candidate descriptions; stop only if a duplicate or overlap is found — otherwise say "No duplicates found" and continue (see [Duplicate Check](#duplicate-check) below)
2. Explore the relevant code to verify the current state before describing it
3. Check the project's recorded decisions for any that constrain this work, and note any one-way decision the work needs that nobody has made (see [Decisions](#decisions))
4. Ask the requester, before drafting, for any bound, exclusion or desired property you would otherwise have to invent (see [Ideal state](#ideal-state) and [Out of scope](#out-of-scope)) — never for an architectural choice
5. Draft using the template and voice rules below
6. Create the ticket using the appropriate platform tool

---

## Duplicate Check

Before writing anything, search the platform for issues that may already cover this ground.

### Step 1 — Search for similar titles

Use the platform's search to find issues whose titles sound similar to the one you're about to create. Cast a wide net: try multiple keyword combinations drawn from the problem domain, not just the exact phrasing the user gave you.

**GitHub:**
```bash
gh issue list --search "KEYWORDS" --state all --limit 20 --json number,title,state
```

**Linear:** use the Linear MCP search tool with keyword queries.

**Jira / Monday:** use available CLI or MCP tools; fall back to asking the user to paste relevant results if no tool is available.

### Step 2 — Read candidate descriptions

For any issue whose title overlaps meaningfully, fetch the full description and read it. Titles alone are not enough — two issues can sound similar but cover different root causes, or the same root cause from different angles.

**GitHub:**
```bash
gh issue view NUMBER --json title,body,state,url
```

**Linear / Jira / Monday:** fetch the full issue body via the relevant tool.

### Step 3 — Recommend one action

Based on what you find, recommend exactly one of the following and explain why:

| Situation | Action |
|---|---|
| Existing issue covers this fully, description is complete | **Stop** — tell the user, link the existing issue, do not create a new one |
| Existing issue covers the same problem but the description lacks context, clarity, or scope | **Stop** — propose specific edits to the existing description and wait for approval |
| Existing issue is correct and complete, but new context or examples should be captured | **Stop** — propose a comment on the existing issue and wait for approval |
| No meaningful overlap found | **Continue** — say "No duplicates found" and proceed to the next quick-start step without pausing |

---

## Epics and Sub-Issues (GitHub only)

Applies when GitHub Issues is the ticketing system. Linear, Jira and Monday have their own
parent/child mechanics — use their native tools instead.

GitHub's official **sub-issue** feature is what to reach for, not task-list checkboxes. Sub-issues
give the parent a real progress bar, give each child a visible parent, and survive renumbering.
Checkbox lists (`- [ ] #12`) are the fallback only when the sub-issue API is unavailable.

### Creating the hierarchy

`gh issue create` has **no `--parent` flag** (checked through gh 2.92). Link after creation via the
REST API, which needs the child's **database id**, not its issue number:

```bash
R=repos/OWNER/REPO
CHILD_ID=$(gh api $R/issues/CHILD_NUMBER --jq '.id')
gh api --method POST $R/issues/PARENT_NUMBER/sub_issues -F sub_issue_id="$CHILD_ID"
```

Works with the ordinary `repo` token scope — no `project` scope required.

### Verifying the link

```bash
gh api $R/issues/PARENT_NUMBER/sub_issues --jq '.[] | "#\(.number)  \(.title)"'
gh api $R/issues/PARENT_NUMBER --jq '.sub_issues_summary'
```

From the child's side, REST exposes the parent as **`parent_issue_url`** — there is no `.parent`
field, so `--jq '.parent.number'` silently prints nothing and looks like a broken link. To read the
parent as a number, use GraphQL:

```bash
gh api graphql -f query='{repository(owner:"OWNER",name:"REPO"){issue(number:N){parent{number title}}}}'
```

### Writing an epic body

An epic uses the same template as any other ticket, with two additions:

- **A coverage table** mapping each externally-defined requirement (a spec, a brief, a checklist in
  a README) to the sub-issue that satisfies it. This is what makes "did we miss anything?"
  answerable at a glance. Fill the numbers in *after* the children exist, then edit the epic.
- **A note naming any sub-issue that has no corresponding requirement** and why it earns its place.
  Unexplained extra scope in an epic reads as scope creep.

Keep the epic's own Ideal state about the aggregate — all children closed, all requirements met —
never about the implementation of any one child.

### Ordering

Create the parent first so children can reference it, and open each child body with `Part of #N`.
Sub-issues list in creation order, so create them in the order you intend to work them.

---

## Title Rules

- **A claim about what will be true once the work is done**, in the present tense — "Large order exports download without timing out"
- **Scannable in a list** — the reader understands what it is without opening it
- **An outcome, not a mechanism** — the claim describes what a user or developer experiences, not how the system achieves it
- **Specific enough to distinguish from similar tickets** — "Mobile users stay signed in after switching apps" not "Sign-in works"

❌ Vague: "Exports work", "Performance is better", "Auth is cleaner"
❌ Task: "Add background export job", "Fix auth bug"
❌ Wish: "Exports should not time out"
❌ Mechanism: "Exports run on a job queue", "Status is cached in SQLite"

---

## Template

```markdown
## Current state

[Lead with the downstream impact, then support it with the observable facts that cause it. No opinions.]

## Ideal state

[Bullet list of observable behaviors when done. Written as user-facing or developer-facing facts — not implementation steps. Each bullet should be independently verifiable.]

## Out of scope

[Only exclusions the requester stated. Omit this section when they stated none.]

## Decisions

**Constraints**

[Each recorded decision this work must respect, linked, with one line on how it binds this work.]

**Open**

[Each one-way decision this work needs that nobody has recorded, phrased as a question, with the category that makes it one-way.]

## Starting points

[2-3 file paths that explain today's behavior. Helps a cold reader orient fast.]

## Depends on

[Optional. List issues that must be complete before this one can start, with a one-line reason each is a hard prerequisite. Omit if no hard dependencies exist.]
```

---

## Voice Rules

### Current state

- Lead with the consequence: one sentence naming the downstream impact
- Follow with the observable facts that cause it — what a person can see or measure today
- Don't editorialize ("unfortunately", "badly", "messy")
- Keep it to 2-4 sentences max
- Whoever re-verifies it after the ticket was created adds a `Checked YYYY-MM-DD` line; a new ticket needs none, since the platform records its creation date

### Ideal state

- Write each bullet as a fact that will be true when the work is done: "X does Y" not "add X" or "implement Y"
- Each bullet is something a person can check in the running system when the ticket closes — these bullets are what make success observable
- A business outcome (fewer support tickets, happier customers) appears only as the reason for a system bullet, after "so that", and only when the link is not obvious
- Where a property has a limit (a size, a time, a count), the bullet states the most demanding value wanted: "An export of up to 1,000,000 orders completes"
- Only the requester supplies a limit. If a property needs one and you do not know it, ask before drafting; never invent a plausible number
- A yes-or-no property gets no limit
- Ideal state is the full scope: anything it does not describe is out of scope
- So each bullet traces to something the requester said. A property you think the work should also have is a question for the requester before drafting, not a bullet
- Ideal state is also the bar for closing: the ticket closes when every bullet holds
- Don't mix in implementation steps — those belong in a PR, not an issue
- The step-by-step verification plan is written during implementation, once the approach is known, not here

### Out of scope

- List only exclusions the requester stated; each one is a decision, and deciding is not the author's job
- An exclusion you think of yourself is a question for the requester before drafting, not a bullet in the ticket
- Omit this section when the requester stated no exclusions

### Decisions

- Look for recorded decisions in `DECISIONS.md`, `docs/decisions/`, `docs/adr/`, `doc/adr/`, `adr/` and `docs/architecture/decisions/`, and search the repo for "ADR"
- A project may record decisions somewhere else, so also check its `CLAUDE.md`, `AGENTS.md`, `README.md` and `docs/README.md` for where it says decisions live
- Read titles first; open a record only when its title touches this work
- Under **Constraints**, link each record that binds this work and say in one line how it binds. Don't restate its reasoning; the record is the source
- A record binds only when its title plainly covers this work. When applying it takes interpretation (does a record about "customer downloads" cover an export only admins can run?), deciding whether it applies is itself a decision: link the record under **Open** as that question instead of asserting it binds
- If the request conflicts with a recorded decision, say so under **Constraints** and do not resolve it. Following the request instead needs a new decision that supersedes the record, and that goes under **Open**
- Under **Open**, list a decision only when the work would introduce one of these: a new external service or dependency; a new persistent data shape or store; a new public interface (an API, an event, a CLI contract, a file format); a new process or runtime; a user-facing pattern people will learn. Any other choice belongs to the implementer and is not listed
- Phrase each open decision as a question and name its category. Name options only if the requester did, and never choose one
- An open decision is settled through the project's decision process (a decision record, an ADR), not by the requester in chat. The requester may answer what users get; how the system provides it stays open. If the repo has `docs/questions/`, suggest a question file for each open decision
- When a list is empty, say so and name where you looked: "No recorded decisions found in `docs/decisions/`, `DECISIONS.md` (searched for ADR)." / "No open decisions."

### Starting points

- Name actual file paths, not directory names
- Pick the files a reader would need to understand the current behavior, not the files where the change should go
- 2-3 max; more than that is noise

### Depends on

- List only hard prerequisites — issues this cannot start without, not issues it would merely benefit from coming after
- One line per dependency: issue reference + why it blocks
- Omit entirely if no hard dependencies exist

---

## What NOT to Do

❌ Describing implementation steps in "Ideal state" — those belong in a PR
❌ Current state that opens with a technical fact instead of the downstream impact
❌ Burying the consequence — "the export runs synchronously" before "large customers cannot export"
❌ An exclusion in "Out of scope" that the requester did not state
❌ A limit in "Ideal state" that the requester did not supply
❌ An "Ideal state" bullet that can only be checked outside the system ("support gets fewer tickets")
❌ An "Ideal state" bullet the requester did not ask for
❌ A ticket that contradicts a recorded decision without saying so
❌ An open decision answered in the ticket, or put to the requester as a chat question
❌ A choice the implementer can easily reverse listed as an open decision
❌ A QA plan or verification steps — they assume an approach nobody has chosen yet
❌ Starting points that name directories instead of files, or point to where the change should go

---

## Example: Good Ticket Description

```markdown
# Large order exports download without timing out

## Current state

Customers with more than about 50,000 orders cannot export their order history, and support receives several tickets a week asking for exports to be run manually. The export request fails with a timeout after 30 seconds for any account above that size, and the page shows a generic error with no suggestion of what to do next.

## Ideal state

- An export of up to 1,000,000 orders completes and the customer receives the file
- A customer who starts a long export can leave the page and still get the file when it is ready
- A customer can see whether an export is still running, finished, or failed
- A failed export tells the customer it failed and lets them start it again

## Out of scope

- New export formats (only the existing CSV)
- Scheduled or recurring exports

## Decisions

**Constraints**

- [004 — Customer downloads are served from expiring links](docs/decisions/004-customer-downloads-are-served-from-expiring-links.md) — the finished export file is delivered the same way

**Open**

- Does a long-running export need a new background process, and which runtime runs it? (new process or runtime)

## Starting points

- `app/orders/export_controller.rb` — handles the export request today
- `app/orders/csv_builder.rb` — builds the file row by row
```

### Why This Works

✅ Title is a present-tense claim about the outcome, not a task or a mechanism
✅ Current state leads with the impact (customers can't export, support load), then supports it with observable facts
✅ Ideal state uses "X does Y" framing and names no mechanism — each bullet is a verifiable fact
✅ Each Ideal state limit (1,000,000 orders) came from the requester
✅ Out of scope lists only the two exclusions the requester stated
✅ Decisions links the one record that binds the work and leaves the background-work question open instead of picking a job queue
✅ Starting points explain today's behavior, not where the change should go

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.