DEV Community

Cover image for You can't set a status in Jira
Kadir Emre Parlak
Kadir Emre Parlak

Posted on

You can't set a status in Jira

Agents write to work trackers now, and the trackers have started modelling them as first-class citizens. Linear's GraphQL schema has AgentSession implements Node and AgentActivity implements Node — not a webhook bolted on the side, but entities alongside Issue and User.

Writing is where this gets interesting, because a tracker is not a database with a status column. It is a state machine with opinions, and every vendor's opinions are different.

Here are three that broke my assumptions, in ascending order of how wrong I was.

GitHub: there is no hierarchy

The easy one. GitHub issues are flat and binary — open or closed. There is no epic, no parent, no workflow. If your process has "this story belongs to that epic", GitHub has nothing to map it onto.

You can emulate it with labels. That works. The important part is what happens when you do: you have now invented a convention that exists nowhere in the provider, and anything reading those issues without knowing your convention sees a flat list with some odd labels on it.

That is a fine trade. It is not fine to make it silently.

Linear: there is no "in review"

Linear has a WorkflowState with a type field, and you would reasonably expect that type to be the vocabulary. Here is the schema's own description of the entity:

A state in a team's workflow, representing an issue status such as Triage, Backlog, Todo, In Review, Done, or Canceled. […] Workflow states have a type that categorizes them (triage, backlog, unstarted, started, completed, canceled).

Read those two lists against each other. "In Review" appears in the first — it is a state a team really has — and nowhere in the second. Review is not a type; it is a state that happens to be typed started, exactly like "In Progress". So you cannot map an abstract "in review" onto a state type. You have to map it onto a specific state, by ID, per team.

(The schema disagrees with itself about the type list, incidentally. The entity description includes triage; the description on the type field itself lists only backlog, unstarted, started, completed, canceled. Whichever is right, it is not a vocabulary you want to hardcode.)

Two further details:

  • WorkflowState.type is String!, not an enum. It is unversioned and the provider can extend it. Anything you switch on today is a guess about tomorrow.
  • WorkflowState.team is Team! — non-null. Workflows belong to teams, so two teams in one workspace legitimately have different states for the same conceptual step, and a single flat mapping cannot represent that.

And triage has no equivalent in most abstract models at all. What is your agent supposed to do with an issue in triage?

Jira: you cannot set the status

This is the one that broke my design.

I assumed a work item had a status, and that writing to it meant setting a field. In Jira it does not. You must first call:

GET /rest/api/3/issue/{issueIdOrKey}/transitions
Enter fullscreen mode Exit fullscreen mode

which returns the transitions currently permitted — which depend on the project's workflow, the issue type, and the item's present state. Then you post the transition id. And the transition may have a screen attached, in which case it demands required fields before it will go through.

So "move this to in progress" is not a write. It is: discover what is possible, match your intent against it, supply whatever the transition screen demands, then act. An agent that assumes otherwise does not fail cleanly — it fails after it has already done half the work.

There is a second trap. Jira's statusCategory looks like the canonical vocabulary, and it has exactly three values: TODO, IN_PROGRESS, DONE. If your model has "ready" or "in review" or "blocked", none of them are derivable from it. You need per-project status mapping, or you need to give up on those concepts.

The thing everybody does instead

The common workaround is to put the vendor's own state name in the prompt. Here is OpenAI's Symphony — an orchestrator that runs coding agents against a tracker — in its reference workflow:

update_issue(..., state: "In Progress")
Enter fullscreen mode Exit fullscreen mode

Its specification is explicit that this is by design. §11.5, "Tracker Writes and Agent Tools":

Symphony does not require first-class tracker write APIs in the orchestrator. Ticket mutations (state transitions, comments, attachments, PR metadata) are typically handled by the coding agent through the selected adapter's provider-native tools.

Both of its required adapter functions are reads. Writing is left to the agent.

That is a reasonable boundary for an orchestrator to draw. But it pushes the problem into the prompt, and a prompt containing "In Progress" is a prompt that breaks when someone renames a column, when a second project uses a different workflow, or when you migrate trackers. It is also precisely the string an agent will hallucinate a plausible variant of.

What I think the shape of the answer is

Three parts, and the second is the one people skip.

Speak in roles, not states. A small abstract vocabulary — backlog → ready → in_progress → in_review → done, plus blocked — that prompts and workflows are written against. Nothing new here; every integration layer has some version of this.

Resolve the mapping once, with a human, and commit it. This is the part that matters. Every runtime approach I have seen has the model discover valid transitions and pick one on each call, which means the answer can differ between two runs for reasons nobody can see. Instead: introspect the provider's actual states at setup, propose a mapping, have a human confirm it, and commit the result to the repo as configuration. Now the mapping is reviewable, diffable, and identical on every run. A wrong mapping becomes a pull request comment instead of a mystery.

Declare the gaps and negotiate them out loud. For every capability a provider lacks, pick a behaviour explicitly: fail loudly, emulate it (GitHub hierarchy via labels), or degrade with a warning. The rule I hold myself to is that a gap which is neither errored nor logged is a bug. This is what stops the abstraction from lying — you are told when you have left the paved road, rather than discovering it from a support ticket three weeks later.

Where I got it wrong

I built this and then found my own model had the same disease.

I had six workflow roles, and blocked was one of them. The documentation right above the code said blocked (orthogonal) — I knew, when I wrote it, that being blocked is not a stage of work but a condition on top of one. The code did not agree with the comment. Transitioning an item to blocked destroyed whatever role it had, so "blocked while in review" was unrepresentable.

Linear says this out loud in the same schema I had just been reading: IssueRelationType is blocks, duplicate, related. Blocking is a relation, sitting right next to the workflow states rather than inside them. Jira and GitLab agree. All three would have broken my design, and the fix — making it a relation instead of a state — is nearly free with zero users and expensive with a hundred.

The general lesson, if there is one: an abstraction designed against two providers is not an abstraction, it is an average. I designed mine against Azure DevOps and GitHub, which are genuinely different from each other, and it still did not survive contact with a third.


This came out of building Baron, an open-source layer that does the above — normalized writes to work trackers from a coding agent, with the role mapping committed to your repo. It is early and I would rather have bug reports than stars.

Top comments (0)