PRACTICAL GUIDE

Interactive GitHub Issue Triage with Copilot Canvas

Level: intermediate Reading time: 30 minutes Outcome: an interactive Issue triage canvas

A large task queue is difficult to process in chat: decisions disappear between explanations, while a spreadsheet rarely provides enough context for confident action. In this guide, you will create a Copilot Canvas that loads GitHub Issues, presents them as cards, records accept, defer, and reject decisions, and applies only the changes that a person has explicitly approved.

What you will build

The finished interface separates decision-making from repository changes. A reviewer first classifies cards inside the canvas. Those choices enter a pending queue but do not immediately change GitHub. A second action previews and applies the approved operations.

GitHub Issues
    ↓ load
cards on the canvas
    ↓ accept / defer / reject
local decision store
    ↓ human review
approved operation plan
    ↓ apply
labels, comments, or closure in GitHub

The minimum useful version should be able to:

  • load open Issues from an explicitly selected repository;
  • display the number, title, author, labels, age, URL, and a short summary;
  • filter cards by label, author, decision, and free-text search;
  • record accept, defer, or reject without immediately writing to GitHub;
  • require a reason for deferred and rejected cards;
  • allow a reviewer to undo a local choice;
  • show every pending operation before synchronization;
  • record the result of each GitHub operation separately;
  • avoid repeating an operation that was already applied successfully.

Why chat and spreadsheets stop working

Issue triage is more than sorting titles. A reviewer must decide whether a report is reproducible, whether it contains enough information, whether it belongs to the project, whether it duplicates another report, and whether the team should act now.

Chat is useful for discussing one ambiguous Issue. It is much less useful for maintaining dozens of parallel states. After several messages, it becomes difficult to see which cards remain undecided, which choices are drafts, and why a particular report was rejected.

A spreadsheet offers a better overview but usually forces the reviewer to switch between a row and the original GitHub page. Long descriptions, labels, comment fragments, and related Issues do not fit naturally into one row. The spreadsheet also tends to blur the distinction between a proposed decision and an action already applied to the repository.

A canvas provides a shared working surface. People can manipulate visible cards while an AI agent loads data, prepares summaries, identifies possible duplicates, and performs only authorized operations.

Concrete case: an incoming bug queue

Consider a repository identified here as OWNER/REPOSITORY. It has accumulated open reports with labels such as bug, needs-info, and enhancement. Some reports describe actionable defects, some lack reproduction steps, and several may share the same underlying cause.

The team wants to run a short review session and give every card one decision:

Decision Meaning Planned GitHub change
accept The Issue is clear enough and belongs in the active queue Add triage:accepted and remove triage:pending
defer A decision depends on additional information or a later date Add triage:deferred and preserve the reason
reject The work is not planned, is a duplicate, or is outside the project scope Add triage:rejected; closing remains a separate action

Define this mapping before generating the interface. A Reject button must not have a hidden meaning. If one reviewer expects it to add a label while another expects it to close the Issue without a comment, the automation will create conflict instead of reducing work.

Separate a decision from an external action

The central rule of this workflow is that clicking a card records intent but does not perform an irreversible operation. Changes reach GitHub only through a separate approval gate.

Each decision therefore needs two independent states:

decision.status:
  undecided | accept | defer | reject

sync.status:
  draft | pending | applying | applied | failed

These fields answer different questions:

  • What did the reviewer decide?
  • Was that decision successfully reflected in GitHub?

Do not combine them into one status. If a network or permission error interrupts synchronization, the human decision must remain available for review and retry.

Step 1. Prepare the repository and access

Open the repository or its local folder in the GitHub Copilot app and start an agent session. The signed-in user needs read access to load Issues. Adding labels, posting comments, or closing Issues requires permission to modify them.

Before generating the canvas, verify the repository with GitHub CLI. The following commands are read-only:

gh auth status

gh repo view OWNER/REPOSITORY \
  --json nameWithOwner,url,hasIssuesEnabled

gh issue list \
  --repo OWNER/REPOSITORY \
  --state open \
  --limit 20 \
  --json number,title,author,labels,createdAt,updatedAt,url

Replace OWNER/REPOSITORY with the real owner and repository name. Do not paste access tokens into the canvas request, generated source files, or decision records. The application and CLI should use their configured authorization.

If hasIssuesEnabled is false, enable Issues in the repository settings before continuing. If gh issue list returns a permission error, the canvas will encounter the same boundary until access is corrected.

Step 2. Define the decision vocabulary

Agree on the labels that represent triage results. The following names are example configuration, not a GitHub standard:

triage:pending
triage:accepted
triage:deferred
triage:rejected
needs-info
duplicate

Inspect existing labels before creating anything:

gh label list \
  --repo OWNER/REPOSITORY \
  --limit 200

If the labels do not exist, create them manually in GitHub or use commands like these after reviewing the names, colors, and descriptions:

gh label create "triage:pending" \
  --repo OWNER/REPOSITORY \
  --color "D4C5F9" \
  --description "Issue is waiting for a triage decision"

gh label create "triage:accepted" \
  --repo OWNER/REPOSITORY \
  --color "0E8A16" \
  --description "Issue was accepted into the working queue"

gh label create "triage:deferred" \
  --repo OWNER/REPOSITORY \
  --color "FBCA04" \
  --description "Issue decision was deferred"

gh label create "triage:rejected" \
  --repo OWNER/REPOSITORY \
  --color "B60205" \
  --description "Issue was rejected during triage"

These commands modify the repository. Run them only after the team has approved the vocabulary. If the repository already has a label system, configure the canvas to use it instead of creating a parallel taxonomy.

Step 3. Generate the canvas

Start or open an agent session in the GitHub Copilot app. In the prompt box, invoke:

/create-canvas

Then provide a precise prompt. Describe both the interface and the boundaries of every action:

Create a canvas for triaging GitHub Issues in
OWNER/REPOSITORY.

Human interface:
- show open Issues as cards;
- display number, title, author, labels, createdAt,
  updatedAt, URL, and a short summary;
- add search and filters for label and decision;
- add Accept, Defer, and Reject buttons;
- require a reason and optional date for Defer;
- require a reason for Reject;
- allow a local decision to be undone;
- show counters for undecided, accepted, deferred,
  and rejected;
- add a separate Pending changes panel.

Safety:
- card actions update only local draft state;
- never close Issues automatically;
- show the exact GitHub operations before applying them;
- apply changes only after explicit confirmation;
- never store tokens or secrets in canvas state;
- record applied or failed after every operation;
- reapplying an applied operation must be safe;
- block application if the source Issue changed after review.

Agent-callable capabilities:
- load_issues;
- get_board_state;
- set_decision;
- clear_decision;
- preview_changes;
- apply_approved_changes;
- retry_failed_changes.

Persist decisions in a JSON artifact inside the extension.
For each record, store repository, issueNumber, issueUrl,
decision, reason, deferredUntil, decidedAt, decidedBy,
sourceUpdatedAt, syncStatus, appliedAt, and error.

Do not overwrite unapplied local decisions during refresh.
If an Issue changed after sourceUpdatedAt, mark the card
as stale and require another confirmation.

When generation completes, the canvas should open in the app’s side panel. Test its controls on a few cards before enabling any function that writes to GitHub.

Step 4. Choose where the canvas lives

A canvas extension can be personal or associated with the project:

  • ~/.copilot/extensions is suitable for a personal prototype on one machine;
  • .github/extensions is suitable for a team canvas stored and reviewed with the repository.

Use personal scope for the first iteration. Move the extension into the repository only after validating the decision model, action names, and state format. A shared canvas becomes part of the project and should be reviewed like other automation code.

The generated structure can vary, but a canvas extension commonly includes metadata, an entry file, and optional artifacts:

issue-triage-canvas/
├── package.json
├── extension.mjs
└── artifacts/
    ├── decisions.json
    └── audit-log.jsonl

Do not assume the exact entry filename. Inspect the generated directory and work with the files that the current version of the app actually created.

Step 5. Inspect the state contract

Store decisions in an explicit JSON artifact. This makes it possible to inspect the state without opening the interface and to restore a session after closing the application.

A minimal decision record can look like this:

{
  "repository": "OWNER/REPOSITORY",
  "issueNumber": 123,
  "issueUrl": "https://github.com/OWNER/REPOSITORY/issues/123",
  "sourceUpdatedAt": "2026-07-27T09:30:00Z",
  "decision": "reject",
  "reason": "The report does not contain reproduction steps",
  "deferredUntil": null,
  "decidedAt": "2026-07-27T10:05:00Z",
  "decidedBy": "current-user",
  "syncStatus": "draft",
  "appliedAt": null,
  "error": null
}

The values are illustrative. The canvas must use the current Issue data, the actual reviewer identity available to the application, and the real decision time.

Four fields are especially important:

  • sourceUpdatedAt reveals whether the Issue changed after it was loaded;
  • reason preserves the reviewer’s rationale;
  • syncStatus distinguishes a draft from an applied repository change;
  • error preserves failure details so that the decision does not need to be made again.

The state file must not contain access tokens, private keys, unnecessary copies of private discussions, or complete Issue bodies when a number, timestamp, status, and reason are sufficient.

Step 6. Keep capabilities narrow

A universal function such as update_issue is too broad. It is difficult to see what it changes, which permission it requires, and whether it is safe to retry. Use small capabilities with explicit inputs and outputs.

load_issues

Load only the required slice of the queue. Do not request every Issue in the repository’s history during the first iteration. Limit the state, labels, and page size:

{
  "repository": "OWNER/REPOSITORY",
  "state": "open",
  "labels": ["triage:pending"],
  "limit": 50
}

set_decision

Update local draft state and send nothing to GitHub:

{
  "issueNumber": 123,
  "decision": "defer",
  "reason": "Logs from the current release are required",
  "deferredUntil": "2026-08-03"
}

preview_changes

Convert decisions into concrete operations. The preview must show more than “update three Issues.” It should identify the Issue, labels, comment, and closure state:

Issue #123
  add labels: triage:deferred
  remove labels: triage:pending
  comment: "Triage: deferred. Reason: current release logs required."
  close: false

apply_approved_changes

Accept only an explicitly confirmed operation list. Before writing, retrieve the current updatedAt value and compare it with sourceUpdatedAt. This is a simple form of optimistic concurrency control: if the record changed, stop and return it for review.

retry_failed_changes

Retry only records with a failed synchronization status. Skip operations already marked applied. This idempotency rule prevents duplicate comments and repeated label changes after a partial network failure.

Step 7. Make each card decision-ready

If the generated card contains only a title, ask Copilot to revise the interface. A reviewer usually needs:

  • the Issue number and a clickable link to the original page;
  • the title and the first useful part of the description;
  • the author, labels, creation time, and last update time;
  • the number of comments when it is available in the loaded data;
  • a short summary clearly marked as model-generated;
  • a possible-duplicate warning with links, not an automatic merge or closure;
  • the current local decision and synchronization status;
  • Accept, Defer, Reject, and Undo controls;
  • a visible stale indicator when the source changes.

A practical layout presents one active card prominently and advances to the next after a choice. A side panel should still show the entire queue and all pending changes, allowing the reviewer to return to any previous decision.

Add keyboard actions only after visible buttons work correctly:

A — accept
D — defer
R — reject
U — undo the local decision
Enter — open Issue details

A keyboard shortcut must never apply changes to GitHub. Keep synchronization behind a separate button and confirmation screen.

Step 8. Constrain AI-generated summaries

A short summary can accelerate review, but it is not a source of truth. The model can omit a reproduction condition, combine unrelated comments, or make an overly confident inference. This is a form of model hallucination.

Separate source data from the agent’s interpretation:

Source data:
- title;
- description;
- labels;
- author;
- timestamps;
- original URL.

AI summary:
- observed problem;
- expected behavior;
- presence of reproduction steps;
- missing information;
- possible duplicates;
- confidence level.

If the summary says that an Issue may be a duplicate, the card should show the proposed original Issue. Without a link and human comparison, the claim remains a hypothesis and cannot justify closure.

Do not load every comment by default. For long discussions, show a compact overview and open the full conversation on demand. This reduces unnecessary model context and keeps manual verification practical.

Step 9. Preview operations before applying them

After classifying several cards, open the pending changes panel. For every record, verify:

  1. the repository owner and name;
  2. the Issue number and title;
  3. the labels to add and remove;
  4. whether a comment will be published;
  5. whether closure is planned and separately authorized;
  6. whether the Issue changed after the decision;
  7. whether defer and reject have meaningful reasons.

Knowing the equivalent CLI operations helps with manual inspection. Accepting an Issue might produce:

gh issue edit 123 \
  --repo OWNER/REPOSITORY \
  --add-label "triage:accepted" \
  --remove-label "triage:pending"

Rejecting without closing could produce:

gh issue edit 123 \
  --repo OWNER/REPOSITORY \
  --add-label "triage:rejected" \
  --remove-label "triage:pending"

gh issue comment 123 \
  --repo OWNER/REPOSITORY \
  --body "Triage: rejected. Reason: reproduction steps are missing."

Closure should remain a separate operation:

gh issue close 123 \
  --repo OWNER/REPOSITORY \
  --reason "not planned"

Do not run these examples literally against Issue #123. The canvas must generate operations for the cards that were actually selected and must wait for confirmation before executing them.

Step 10. Preserve the decision history

Current state alone is insufficient. If a reviewer changes a decision from reject to accept, the team may need to know when and why that happened. Add an append-only audit log whose previous events are not overwritten.

{
  "eventId": "decision-123-20260727T100500Z",
  "repository": "OWNER/REPOSITORY",
  "issueNumber": 123,
  "action": "decision_changed",
  "from": "undecided",
  "to": "reject",
  "reason": "Reproduction steps are missing",
  "actor": "current-user",
  "createdAt": "2026-07-27T10:05:00Z"
}

Record synchronization separately:

{
  "eventId": "sync-123-20260727T101200Z",
  "repository": "OWNER/REPOSITORY",
  "issueNumber": 123,
  "action": "github_update",
  "status": "applied",
  "operations": [
    "add:triage:rejected",
    "remove:triage:pending"
  ],
  "createdAt": "2026-07-27T10:12:00Z"
}

This history reconstructs a review session and reveals whether a local choice reached GitHub. It supplements the Issue history with draft and failed operations; it does not replace GitHub’s own event history.

Verify the result

Do not begin with the full production queue. Create a test Issue or select a safe card on which the team has authorized label changes. Then perform the checks below.

1. Loading

  • The canvas shows the same number, title, URL, and labels as GitHub.
  • A label filter does not silently hide cards with pending local decisions.
  • Refreshing the source does not duplicate the same card.
  • Cards use repository + issueNumber as their stable identity.

2. Draft decisions

  1. Press Accept on the test card.
  2. Confirm that the card enters the local accepted group.
  3. Refresh the corresponding page in GitHub.
  4. Verify that no labels or comments have changed yet.

If GitHub changes immediately after the card click, the separation between decision and execution is broken.

3. Undo

  1. Undo the local choice.
  2. Confirm that it disappears from the pending queue.
  3. Close and reopen the canvas.
  4. Verify that the cancelled operation does not reappear.

4. Approved application

  1. Accept the test Issue again.
  2. Open the operation preview.
  3. Compare the repository, number, and labels with the original card.
  4. Confirm the operation.
  5. Open the Issue in GitHub and inspect its final labels.
  6. Verify that syncStatus changed to applied.

5. Safe retry

Attempt to apply the same decision again. The canvas should report that the operation was already completed. It must not post another identical comment.

6. Stale data protection

  1. Record a decision but do not apply it.
  2. Edit the Issue description or add a GitHub comment.
  3. Refresh the canvas data.
  4. Verify that the card receives a stale status.
  5. Confirm that application is blocked until the reviewer checks the updated Issue.

7. Permission failure

When a safe test repository or read-only test identity is available, attempt an approved write without sufficient permission. The operation should become failed with a useful error, while the original decision remains stored.

8. Partial failure

If one operation in a batch fails, successful operations should stay applied and failed operations should remain retryable. The canvas must not replay the entire batch blindly.

A canvas is ready when every confirmed decision can be traced from the card click to the resulting GitHub state—not merely when cards move smoothly on screen.

Failure cases and fixes

Cards are duplicated after refresh

The implementation uses the array position or title as the identity. Use repository + issueNumber. Titles can change, and different Issues can share the same title.

Local decisions disappear during reload

The loader replaces the entire state with the latest GitHub response. Merge remote fields with local decisions using the stable identity. Do not delete an unapplied draft merely because the current filtered response does not contain its card.

Reject closes the Issue immediately

The canvas has combined classification with an external action. Separate set_decision from apply_approved_changes. Make closure an additional flag that is disabled by default.

Comments are posted twice

The application replays the entire batch after a partial failure. Give each operation a stable key and persist its individual result:

operationKey =
  repository + ":" +
  issueNumber + ":" +
  decisionVersion + ":" +
  operationType

A retry must skip an operation already marked applied.

A decision is applied to an outdated Issue

The canvas does not compare source versions. Store sourceUpdatedAt, retrieve the current updatedAt before writing, and require review when the values differ.

Filters hide unfinished work

A selected label can remove a card with a draft decision from the visible queue. Keep pending counters and a dedicated drafts panel visible independently of the active filter.

Large loads begin failing

The query may be too broad or may encounter a GitHub rate limit. Load Issues in pages, cache cards already fetched, and avoid refreshing every card through a separate request.

The AI summary is treated as evidence

Separate summaries visually, link back to the original Issue, and prevent the model from closing a report solely because of its own classification. The human reviewer remains responsible for the decision.

The log contains unnecessary private data

The canvas stores full descriptions and discussions even though recovery requires only identifiers, timestamps, statuses, and reasons. Minimize the artifact and exclude secrets. A private repository remains private, but an exported decision file can create a separate disclosure risk.

Limitations

  • The canvas accelerates repeated decisions but does not determine product priorities for the repository owner.
  • An AI summary can omit a critical detail from a long discussion. Open the original Issue before rejection or closure.
  • Duplicate detection is probabilistic. Similar wording does not prove that two defects have the same cause.
  • The signed-in user’s permissions limit the canvas. The interface must not attempt to bypass repository policy.
  • Canvas extension structure and available GitHub Copilot app capabilities can change. Inspect the files generated by the version you are using.
  • A local JSON artifact is convenient for one reviewer but does not solve concurrent editing.
  • A team version needs locking, version checks, or a merge strategy so two reviewers cannot silently process the same Issue.
  • Bulk closure remains risky even with a good interface. Begin with labels and small, manually reviewed batches.
  • The canvas is not a backup or a rollback system. Preserve previous labels and states if bulk changes may need to be reversed.

Move from prototype to team use

After testing several Issues, move the extension into project scope and include its files in the normal review process. Before shared use, document:

  • the repositories the canvas may access;
  • the decision and label vocabulary;
  • required reasons for rejection and deferral;
  • the maximum approved batch size;
  • whether automated comments are permitted;
  • who may authorize Issue closure;
  • how long decision history is retained;
  • how a partial synchronization failure is recovered.

For the first working session, set a small batch limit—for example, no more than ten confirmed cards per application. This is a safety rule, not a performance claim. Compare the decision log with GitHub after each batch before increasing the scope.

If several people triage simultaneously, store the decision owner and version. Warn the reviewer when another participant has already created a newer decision for the same Issue.

Final checklist

  • The repository is explicit and is not inferred from an unrelated current directory.
  • Every card is identified by repository and Issue number.
  • Accept, Defer, and Reject have documented meanings.
  • A reason is mandatory for defer and reject.
  • Card clicks modify local draft state only.
  • The complete operation list appears before synchronization.
  • Issue closure is disabled by default.
  • A stale card blocks application until it is reviewed again.
  • Every external operation receives an individual status.
  • A successful operation is not repeated after restart.
  • Errors remain attached to their decisions.
  • The log contains no tokens or unnecessary Issue content.
  • The final result is verified both in the canvas and on GitHub.

Conclusion

A useful Copilot Canvas for Issues is not merely a board with three columns. It is a controlled process in which the source card, the reviewer’s decision, and the resulting GitHub operation remain distinct and traceable.

Begin with read-only loading and local choices. Add operation previews, explicit confirmation, stale-data protection, individual synchronization statuses, and an append-only history. Allow repository writes only after a small test batch has been inspected manually.

The result turns a long queue into a practical decision surface: every card presents the required context, each choice takes one clear action, the reason remains available, and every repository change can be verified or retried safely.

← All practical guides · Lab glossary →