Skip to main content

How to work with agents

An agent is one more client of the API, and there is no separate "agent API". This page collects the endpoints and the conventions that are most important when the caller is an LLM. The agent reads the full context of a task in one request, keeps each payload small, exchanges markdown and not rich-text JSON, and searches the past work to see if somebody solved the problem before.

Everything on this page uses the same authentication with a personal access token as the other parts of the API.

tip

The better method for most agents is a connection over MCP, and not a direct call to these endpoints. See How to connect an MCP client. The tools there use the REST layer below. This page describes that layer, for a client that you build directly against the API.

An agent account, and what it can do

An agent account is its own identity in the workspace. The workspace therefore records each edit against the agent, and not against the person who connected it. Make an agent account from Settings → Agents, or with POST /v1/users/agents. Only an administrator can use that endpoint.

An agent has scopes, and the server checks them on every request:

ScopeWhat it covers
readRead the board, search, and get a task.
writeFile, update, comment, and close.
deleteRemove an issue, a comment, a label, and the other records.

The HTTP method of a request gives the scope that the request needs. GET reads, DELETE deletes, and every other method writes. A route can declare something different. A few reads arrive as a POST, because their input travels in the body. POST /v1/issues/filter is the board itself, and it counts as a read.

A new agent gets read and write. You give delete deliberately, so an agent does the whole issue workflow without the one verb that nobody can undo. To change the scopes, send scopes when you make the account:

{ "name": "Release Bot", "scopes": ["read", "write", "delete"] }

A request outside the scopes of an agent gets a 403, and the error names the scope that the request needed. An agent from a time before the scopes has the default scopes, and not all of them. A person has no scopes, because this applies to an agent account only.

The MCP endpoint is an exception, because one POST there covers every tool, and some of those tools only read. The tools reach the API over loopback, so the server checks the scope of the work that they do.

Markdown, and not Tiptap JSON

The server holds a rich-text field as Tiptap JSON. An issue description and a comment body are rich-text fields. The API converts the format at the edge, so an agent never touches Tiptap:

DirectionIssue fieldComment field
ReaddescriptionMarkdownbodyMarkdown
WritedescriptionMarkdownbodyMarkdown

A read returns the markdown field with the raw description or body. A write accepts one field or the other. If you send the raw field and the markdown field together, the raw Tiptap field wins. Therefore send only descriptionMarkdown or bodyMarkdown, unless you write Tiptap deliberately.

How to read the full context of a task in one call

GET /v1/issues/:issueId/context

This endpoint returns everything that an agent needs to start work on an issue. Each id already has its name for a reader, so you make no second call to translate a UUID. No raw Tiptap JSON is in the response.

{
"id": "…",
"key": "ENG-42",
"title": "Connections exhausted under load",
"descriptionMarkdown": "…",
"state": { "id": "…", "name": "In Progress", "category": "STARTED" },
"assignee": { "id": "…", "fullname": "Jane Doe" },
"team": { "id": "…", "identifier": "ENG", "name": "Engineering" },
"labels": [{ "id": "…", "name": "bug" }],
"priority": 2,
"estimate": null,
"dueDate": null,
"project": null,
"cycle": null,
"parent": { "id": "…", "key": "ENG-40", "title": "…" },
"subIssues": [{ "id": "…", "key": "ENG-43", "title": "…", "stateCategory": "COMPLETED" }],
"relations": [{ "type": "BLOCKED", "issue": { "id": "…", "key": "ENG-12", "title": "…" } }],
"linkedIssues": [{ "id": "…", "url": "https://github.com/…", "title": "PR #12" }],
"comments": [
{
"id": "…",
"author": { "id": "…", "fullname": "Jane Doe" },
"createdAt": "…",
"bodyMarkdown": "…",
"replies": []
}
],
"history": [
{ "at": "…", "actor": "Jane Doe", "change": "state", "from": "Todo", "to": "In Progress" }
],
"createdAt": "…",
"updatedAt": "…"
}

Notes:

  • The response holds no deleted comment, no deleted relation, and no deleted sub-issue.
  • relations[].type always takes the point of view of the issue that you ask for. The database holds the row ENG-50 BLOCKS ENG-42. When you ask for the context of ENG-42, that row reads as BLOCKED.
  • history is short. It holds one entry for each change that a person can read: state, assignee, priority, estimate, team, project, cycle, parent, and label. Each id has its name. The response holds no row where nothing changed for a reader.

Two parts of the same data have their own endpoint, for when you need only that part:

GET /v1/issues/:issueId/comments # in time order, with the replies one level below
GET /v1/issues/:issueId/history # the short history array

How to close an issue with a resolution

There is no endpoint for "close". To close an issue, move it to a workflow state with the COMPLETED category:

  1. Send GET /v1/:teamId/workflows and select the state with category: "COMPLETED".
  2. Send the explanation as a comment. Use POST /v1/issue_comments?issueId=… with bodyMarkdown.
  3. Send POST /v1/issues/:issueId?teamId=… with { "stateId": "<completed state>" }.

The order is important, and you send the comment before the change of state. The search returns the resolution of an issue from the last top-level comment at or before the change into COMPLETED. The next section gives more detail.

How to find a fix from the past

GET /v1/search?workspaceId=…&query=pg+pool&stateCategory=COMPLETED

The search index covers the title of an issue, its description, and the bodies of its comments. You therefore find a fix that only a comment describes. These are the parameters:

ParameterDescription
queryFree text.
workspaceIdOptional. The default is the workspace of your session.
limitThe maximum number of hits. The default is 10, and the maximum is 100.
thresholdThe threshold for the vector distance. The default is 0.8.
stateCategoryThe workflow categories for the search, with a comma between two of them. For example COMPLETED, or STARTED,COMPLETED.

Only query is necessary. workspaceId defaults to the workspace of your session. If you omit a numeric parameter, or if the server cannot parse it, the server uses the default value. The request does not fail.

Each hit holds descriptionMarkdown, stateCategory, and resolutionSnippet. The snippet is the first 500 characters of the comment that explains the resolution. One call therefore tells you that a similar issue exists, and how somebody fixed it. GET /v1/search/similar_issues?workspaceId=…&issueId=… returns the same fields.

The server makes the embeddings again after a change to an issue or to one of its comments. It does this work asynchronously, so a new comment becomes searchable in a few seconds, and not immediately.

The search runs fully inside your deployment. Typesense makes the embeddings in its own process, with a model that it includes. The server sends no title, no description, and no comment to an external service.

How to keep a list payload small

POST /v1/issues/filter

By default this endpoint returns a full row for every issue that matches, and that payload is expensive for a context window. If you send page, perPage, orderBy, or view, the endpoint returns pages instead:

{
"filters": { "state": { "filterType": "IS", "value": ["<stateId>"] } },
"workspaceId": "…",
"page": 1, // the default is 1
"perPage": 50, // the default is 50; a value above 200 gets a 400
"orderBy": "updatedAt", // updatedAt (the default) | createdAt | number | priority, always from high to low
"view": "list" // list (the default) | full
}
{ "issues": [ … ], "page": 1, "perPage": 50, "total": 123 }
  • view: "list" returns id, key, title, stateId, stateCategory, assigneeId, priority, labelIds, projectId, and updatedAt. It returns no description.
  • view: "full" returns every column of the issue, and descriptionMarkdown.

If you send none of the four parameters, the endpoint keeps its original behaviour and returns an array of full issues. A client from an earlier time therefore continues to work.