← All guides

How to write a GitHub issue for an AI coding agent

You are about to hand an issue to a coding agent rather than to a colleague, and the wording that would have been fine for a colleague is not going to be enough. What the agent needs is closer to a specification: a boundary, a starting point and a definition of done, so that it can start, and know when it has finished, without you in the loop.

This page assumes the five things any bug report needs are already there. It is about the layer on top: the fields a human reader never asks for, because a colleague would simply ask you, and an agent will not.

The short version

  • Say where the change belongs, and what it must not touch.
  • Name the entry point, or an exact string that only exists there.
  • Paste the literal failure, and something it can run to see the failure itself.
  • State done as a check: a command, a test name, an observable result.
  • Keep it to one finishable thing, and say roughly how big you think it is.
  • Leave out your diagnosis, or mark it clearly as a guess.

One thing up front, since this site is published by a tool that reads issues. WhatProblem answers issues written by people and skips ones opened by bots, so if your agent files under its own GitHub App identity nothing here will respond to it. There is a section at the end on what to do about that.

An agent will not ask, it will pick

A colleague who reaches a gap in your issue stops and messages you. That is not politeness, it is the cheapest way to avoid losing an afternoon to a guess. These tools are built to the opposite specification: they are built to reach a result without stopping, so a gap is not an interruption. It is a blank filled in from priors and from whatever the nearby code seems to suggest.

The consequence runs one way. "The export is slow" sent to a person comes back as a question: slow compared to what, and for whom? Sent to an agent, it comes back as a caching layer. Both are reasonable given the input, and only one of them can be wrong quietly. Filing in bulk makes this worse, not better, because nobody reads the batch before the work starts.

Say where it belongs, and where to start

The boundary is the field a human reader never needs. Your colleagues already know that the vendored directory is off limits and that one module is mid-rewrite this week. An agent knows neither, and an instruction with no boundary produces a diff with no boundary. Two lines: the directory the change belongs in, and what it must leave alone.

Then the entry point. Most of these tools open by searching the repository, and the first search sets the direction of the run. Give it the function, the file or the route. If you do not know, give it a string that exists only in the right place: the log line, the feature flag, the config key, the exact wording of the error. "Somewhere in the payments code" is worse than saying nothing, because it sounds precise and sends the search to the wrong floor of the building.

Paste the literal failure, and give it something to run

Paste the text, not your summary and not a screenshot: the full message, the stack trace, the log line, in the original wording and casing. The reproduction steps guide covers why this matters for human readers, and it all applies here, with one addition. A literal string is a search that lands in the right file on the first attempt.

The agent-specific half gets skipped: give it something it can run. A failing test, a curl command, one command that ends in the wrong output. Anything that can make the failure happen can also tell whether the failure has stopped, and work that can be checked gets corrected before you see it. Without that, the run is aimed at your description of the failure rather than the failure.

Make done something it can check without you

Done written as a check is the target the work aims at. npm test -- schedule passes. The endpoint returns 429 with a Retry-After header. That log line stops appearing.

Stating done observably is worth doing for human readers too, and agreeing what done means covers that case. The difference here is mechanical rather than social: with a person a fuzzy definition of done produces a conversation, and with an agent it produces a pull request that satisfies the fuzzy version.

One finishable thing, and roughly how big

A compound issue costs more here than it does with people. Someone who picks up an issue containing three requests does the first and comes back to ask about the rest. An agent does all three in one diff, and the review is now about three things at once. When one issue is really three has the test for spotting one and how to split it.

Then add your estimate of the size and whether you want a plan first. "Small, one file, go ahead" and "this probably touches the scheduler and the queue, so show me a plan before you write anything" produce very different runs from the same description. Your estimate does not have to be right to be useful. It tells the agent when to stop and check rather than carry on.

Leave out your diagnosis

This omission matters more for a machine reader, and it is the one most likely to be in an issue your agent wrote, because agents file conclusions. A person discounts your theory: they read "the cache is stale" as a hint and go and look. An agent reads it as the specification and changes the cache. If the theory was wrong, you now have a correct implementation of the wrong change and the original failure is still there, harder to see than before.

Say what you observed, and put the theory on its own line, marked as a guess. Filing a proposal in place of a problem is the failure the feature request guide names, and when the request arrives already solved is the longer treatment. Two further omissions are specific to a reader that is not a person: links it cannot open, such as a Slack thread or an internal dashboard, and anything that exists only inside a screenshot.

The same issue twice

What gets filed

Fix timezone handling

The scheduler does not respect user timezones. Reports should be sent in the user's local time. Implement timezone-aware scheduling across the application. Priority: high.

The same issue, written to be acted on

Problem: a report scheduled for 09:00 by a user in Europe/Berlin is sent at 09:00 UTC. It arrives two hours late in summer and one hour late in winter. Users in UTC are unaffected.

Where: scheduling only, in src/scheduler/. Do not change the renderer, the email templates or the stored schedule format.

Start at: nextRunAt() in src/scheduler/schedule.ts. It is the only place a send time is computed.

What happens: the job log shows scheduled=09:00 fired=2026-07-14T09:00:00Z for a user whose stored timezone is Europe/Berlin.

Done when: npm test -- schedule passes with a new case for a non-UTC timezone, including one that crosses a daylight saving boundary.

Size: small, probably one file. Go ahead without a plan.

What a finished specification contains

This is not a format invented for the article. When our own command line requirements tool decides an input is complete enough to build from, it returns a completeness score, a status of ready, and a spec made of these fields:

  • title, type and a description of the problem.
  • acceptance_criteria, the checkable list.
  • files_likely_affected, where the change is expected to land.
  • estimated_complexity and suggested_workflow.
  • dependencies and risks.

When the input is not complete enough it returns needs_review or blocked, a partial_spec carrying only title, type and estimated_scope, and a list of clarifying_questions in which each question carries the question, a why and a priority.

That schema belongs to the command line tool. The GitHub App on the Marketplace does not produce it: the App works in the issue thread, does not read your code, and never touches pull requests. The field names are worth having anyway, because an input that cannot fill them does not become fillable by being handed to something faster.

If your agent files under its own identity

GitHub records an account type for the author of every issue. An App that authenticates as an installation files under its own identity, with an account type of Bot and a login ending in [bot]. WhatProblem reads that and skips those issues on purpose, because automated issues from CI, linters and dependency bots are usually already actionable, and skipping them leaves your monthly allowance for the issues your team writes. It posts a note in the repository explaining the silence, at most one per repository a month, and is otherwise quiet about it.

An agent filing through a personal access token is a user account as far as GitHub is concerned, and gets an answer like anybody else. So does an App acting on a user access token, which records the user as the author rather than the bot. Only an App filing under its own installation identity is skipped, and the fix for that is to comment !whatproblem analyze on the issue yourself. The author of the comment is what counts, so a person asking is enough to start the analysis on an issue a machine opened.

What this will not do

Writing an issue this way makes one issue legible to one agent on one run. It does not find the duplicate somebody filed last March, does not decide what is worth doing first, does not route the work to anyone, and does not survive being pasted into a thread alongside three other requests. Those are separate problems, and no issue format solves any of them.

It also does not take you out of the loop. It moves you earlier in it, which is the trade: the minutes spent on the boundary and the definition of done are minutes not spent reading a diff that touched half the repository. If you are at the other end of this, picking tickets up rather than filing them, the developer's view is the same argument running backwards.

WhatProblem does this in the issue thread

It reads new GitHub issues and asks what is missing, a minute or two after they are opened, before anyone on your team has to.

Install from GitHub Marketplace