Issues API
REST endpoints for issues — CRUD, dispatch to an agent, comments, activity timeline, time entries, custom field values, batch delete.
The Issues API is the canonical surface for the Plan → Issues features. Mounted at /api/v1/issues. Every endpoint follows the common conventions — JSON in/out, cursor pagination, RBAC-gated by issues.* permissions.
Endpoints
| Method · Path | Purpose | Permission |
|---|---|---|
GET /api/v1/issues | List with filters | issues.read |
POST /api/v1/issues | Create | issues.create |
DELETE /api/v1/issues | Bulk delete (body: {ids: []}) | issues.delete |
GET /api/v1/issues/{id} | Read with embedded relations | issues.read |
PATCH /api/v1/issues/{id} | Update any field | issues.edit |
DELETE /api/v1/issues/{id} | Single delete | issues.delete |
POST /api/v1/issues/{id}/dispatch | Dispatch to an agent | issues.dispatch |
GET /api/v1/issues/{id}/comments | List comments | issues.read |
POST /api/v1/issues/{id}/comments | Add a comment | issues.edit |
GET /api/v1/issues/{id}/activities | Activity timeline | issues.read |
GET /api/v1/issues/{id}/time-entries | Time entries | issues.read |
GET /api/v1/issues/{id}/custom-fields | Custom field values | issues.read |
PUT /api/v1/issues/{id}/custom-fields | Set custom field values | issues.edit |
Routed in internal/server/router/router.go under r.Route("/issues", ...).
Filters
GET /api/v1/issues accepts these query params (per handlers/issues.go ListIssues):
| Param | Type | Notes |
|---|---|---|
status | backlog | todo | in_progress | in_review | done | cancelled | Single value (one status at a time) |
teamId | string | |
projectId | string | |
assigneeId | string | Filter by human assignee |
parentId | string | Sub-issues of a parent |
sprintId | string | |
initiativeId | string | |
limit, cursor, updatedSince, includeCount | Standard pagination |
The handler accepts seven domain filters today. Filtering by priority, type, assigneeAgentId, label, or full-text search happens client-side after fetching — bring them up if you need them as first-class server-side filters.
Read shape
GET /api/v1/issues/{id} returns the full graph:
{
"id": "iss_a1b2c3",
"number": 42,
"title": "Fix login redirect loop",
"description": "Steps to reproduce…",
"status": "in_progress",
"priority": "high",
"type": "bug",
"teamId": "team_back",
"orgId": "org_default",
"projectId": "proj_auth",
"assigneeId": "user_alice",
"assigneeAgentId": null,
"parentId": null,
"sprintId": "sprint_q2_w3",
"initiativeId": "init_oidc_ga",
"startDate": "2026-05-01",
"dueDate": "2026-05-12",
"estimate": 5,
"summary": "AI-generated synopsis…",
"workflowId": "wf_…",
"createdAt": "2026-05-04T22:14:00Z",
"updatedAt": "2026-05-05T09:31:08Z",
"labels": [{"id":"lbl_a","name":"auth","color":"#ef4444"}],
"customFields":[{"fieldId":"cf_severity","fieldType":"select","textValue":"sev-1"}],
"team": { "id":"team_back","name":"Backend","identifier":"BACK" },
"project": { "id":"proj_auth","name":"Auth Refresh" },
"assignee": { "id":"user_alice","name":"Alice","email":"alice@…" },
"creator": { "id":"user_pm","name":"Pat" }
}
The list endpoint returns the same shape but with embedded relations summarized (no full team/project objects — just IDs).
Create
curl -X POST http://localhost:3000/api/v1/issues \
-H "Authorization: Bearer $PFAI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Migrate admin login to OIDC",
"type": "bug",
"priority": "high",
"teamId": "team_back",
"projectId": "proj_auth",
"labels": ["auth", "production"],
"assigneeId": "user_alice"
}'
Required: title, teamId. Defaults: status=backlog, priority=none, type=task. The response is the freshly-created issue including the auto-assigned number.
Update
# Move to in_review, change priority, add a label
curl -X PATCH http://localhost:3000/api/v1/issues/iss_a1b2c3 \
-H "Authorization: Bearer $PFAI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"status": "in_review",
"priority": "urgent",
"labels": ["auth", "production", "needs-review"]
}'
labels is replace-style (the array you send is the new full label set). To add without replacing, fetch first and merge. Comments, activity, and custom fields use their own dedicated endpoints rather than fields on the main resource.
Dispatch to an agent
curl -X POST http://localhost:3000/api/v1/issues/iss_a1b2c3/dispatch \
-H "Authorization: Bearer $PFAI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"agentId": "claude-code",
"branch": "main",
"prompt": "Investigate the redirect loop and fix it"
}'
Response includes the agentExecutionId. The dispatch path:
- Sets
assigneeAgentIdon the issue - Creates an
AgentExecutionrow - Provisions a
per_executioncontainer - Mirrors the run into the unified
workflow_executionview
Stream progress via GET /api/v1/agents/executions/{execId}/events (SSE) or pfai wait agent-execution/{execId}.
Bulk delete
curl -X DELETE http://localhost:3000/api/v1/issues \
-H "Authorization: Bearer $PFAI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"ids": ["iss_a1b2c3","iss_d4e5f6","iss_g7h8i9"]}'
Returns 204 on full success. Partial failures (some IDs missing or unauthorized) come back as 200 with {"deleted":[...], "errors":[...]}.
Activity timeline
curl http://localhost:3000/api/v1/issues/iss_a1b2c3/activities \
-H "Authorization: Bearer $PFAI_TOKEN"
Returns the chronological feed of activity events. Twelve types tracked — issue_created, status_changed, priority_changed, assignee_changed, title_changed, description_changed, labels_changed, comment_added, comment_deleted, time_logged, agent_dispatched, workflow_assigned. Each entry carries oldValue/newValue strings for diffing.
See also
What issues are, the data model, six statuses, agent assignment, sub-issues.
Trigger workflows that act on issues — events fire on every state transition.
Pagination, filtering, soft-delete behavior.
Every endpoint here has a CLI wrapper — pfai task list/view/create/dispatch/comment.