Skip to main content
Issues are crew-scoped tracker items: each carries a human-readable identifier (e.g. ENG-42), a status flow, and lifecycle operations for agent execution. Beyond CRUD, the API covers comments, relations, an activity log, workspace labels, and projects that group issues with tracking metadata. Under the hood, issues are stored in the missions table with mission_type = 'issue' — they share infrastructure with missions but keep their own status flow, identifiers, and lifecycle.
All issue endpoints require authentication and workspace context.

Endpoints


Issues CRUD

Create, read, update, and delete issues. Most write operations require OWNER, ADMIN, or MANAGER role and emit a WebSocket event on workspace:{workspaceId}.

List Issues (Workspace)

Returns issues across the workspace with filtering, sorting, and pagination. Query Parameters: Response: 200 OK

Get Issue (Workspace-scoped)

Get an issue by its identifier (e.g., ENG-42) across the entire workspace. No crew ID needed. Request: identifier is required; workspace_id selects the workspace. No request body. Response: 200 OK — full issue object with labels and comment count.

Create Issue

Auth: OWNER, ADMIN, or MANAGER role The crew must have a LEAD agent. The issue identifier is auto-generated from the crew’s issue_prefix (or first 3 characters of the slug) and an auto-incrementing counter. Request Body:
Response: 201 Created — issue object with generated number and identifier. WebSocket event: issue.created on workspace:{workspaceId}

Get Issue (Crew-scoped)

Get an issue by its identifier within a specific crew. Includes labels and comment count. Request: crewId and identifier are required path parameters; workspace_id selects the workspace. No request body. Response: 200 OK — full issue object.

Update Issue

Auth: OWNER, ADMIN, or MANAGER role Request Body: All fields optional. Status Transitions: When status changes to DONE, CANCELLED, or DUPLICATE, completed_at is automatically set. Significant changes (status, assignee, priority) are logged as activity entries. Response: 200 OK — updated issue object. WebSocket event: issue.updated on workspace:{workspaceId}

Delete Issue

Hard-deletes the issue. Only issues in BACKLOG or CANCELLED status can be deleted.
Request: crewId and identifier are required path parameters; workspace_id selects the workspace. No request body. Auth: OWNER, ADMIN, or MANAGER role Response: 204 No Content WebSocket event: issue.deleted on workspace:{workspaceId}

Lifecycle Actions

Drive an issue through agent execution: start, stop, and review.

Start Issue

Starts execution of a BACKLOG or TODO issue. The issue must have an assignee. This:
  1. Creates a synthetic chat session for the mission
  2. Creates or resets tasks (resets existing tasks to PENDING for re-runs)
  3. Transitions status to IN_PROGRESS
  4. Starts the MissionEngine asynchronously
Auth: OWNER, ADMIN, or MANAGER role Response: 200 OK
WebSocket event: issue.started on workspace:{workspaceId}

Stop Issue

Stops a running issue. Cancels all running/pending tasks and sets status to CANCELLED. Only issues in IN_PROGRESS or REVIEW status can be stopped.
Request: crewId and identifier are required path parameters; workspace_id selects the workspace. No request body. Auth: OWNER, ADMIN, or MANAGER role Response: 200 OK

Review Issue

Submit a review decision for an issue in REVIEW or IN_PROGRESS status. Auth: OWNER, ADMIN, or MANAGER role Request Body:
  • approve: Transitions issue to DONE, adds approval comment and activity
  • request_changes: Transitions issue to TODO, optionally reassigns, adds comment and activity
Response: 200 OK

Bulk

Apply one set of field updates across many issues from the board/list multi-select toolbar.

Bulk Update

Apply a single set of field updates to up to 100 issues at once. Used by the board/list views’ multi-select toolbar. Each issue is processed in its own statement — partial success is possible: a request touching 50 ids where 3 have invalid status transitions returns {"updated": 47} rather than rolling back. Auth: OWNER, ADMIN, or MANAGER role (create permission). Request Body:
Response: 200 OK
When updated > 0, an issues.bulk_updated WebSocket event is broadcast on workspace:{workspaceId} with the count. Per-issue status changes additionally produce activity entries (status_changed, details suffixed with (bulk)).

Sub-issues

Issues can nest via parent_issue_id. List the children of one parent.

List Sub-issues

Returns the children of one parent issue — rows where parent_issue_id equals the resolved parent id. Ordered by sort_order ASC, created_at ASC so the response matches the order operators see in the UI’s nested list. Path parameters: Response: 200 OK — array of full issue objects matching the GET /api/v1/issues shape. Empty array (never null) when the parent has no sub-issues.

Activity

A read-only audit trail of significant issue changes.

List Activity

Returns the activity log for an issue (status changes, assignments, reviews). Limited to 50 most recent entries. Response: 200 OK
Activity Actions: created, status_changed, assignee_changed, priority_changed, description_changed, parent_changed, relation_added, code_link_added, code_link_removed, review_approved, review_changes_requested, task_completed, task_failed description_changed records that the description was rewritten, never the text itself — details carries only lengths (description updated (128 → 342 chars)). An issue description is unbounded user input, and an audit row is not a revision store; if you need the previous text, read the issue history in your own tooling before you PATCH. Every action above also writes a Crew Journal entry, which is what notifications are routed from — including the ones an agent produces through the internal API. A client that reads the activity list sees the same events a Slack/ntfy subscriber does.

Runs

The agent executions behind an issue. Issues run on the mission engine (not pipelines), so a “run” is a mission task backed by an assignments row.

List Runs

Lists the agent task-runs for an issue — each mission task joined to its assignments row (execution status, timing, result, error). Newest-first, capped at 100 rows. Request: crewId and identifier are required path parameters; workspace_id selects the workspace. No request body. Auth: any authenticated workspace member (read). Response: 200 OK — a JSON array (never null).

Comments

Threaded discussion on an issue, ordered oldest-first.

List Comments

Returns all comments on an issue, ordered by creation date ascending. Request: crewId and identifier are required path parameters; workspace_id selects the workspace. No request body. Response: 200 OK

Create Comment

Auth: OWNER, ADMIN, or MANAGER role Request Body:
Response: 201 Created — comment object with author_type: "user".

Relations

Link issues to one another (blocks, relates to, duplicate of). Relation types are flipped when viewed from the target side.

List Relations

Returns all relations for an issue. Relation types are flipped when viewed from the target side (e.g., blocks becomes blocked_by). Request: crewId and identifier are required path parameters; workspace_id selects the workspace. No request body. Response: 200 OK

Create Relation

Auth: OWNER, ADMIN, or MANAGER role Request Body:
Internally, blocked_by is stored as blocks with source/target swapped for normalization. Response: 201 Created

Delete Relation

Auth: OWNER, ADMIN, or MANAGER role Request: relationId is a required path parameter and workspace_id selects the workspace. No request body. Response: 200 OK with {"status": "ok"}
Attach the pull request / merge request that resolves an issue. The provider is recognised from the URL’s path grammar (/pull/<n> → GitHub, /-/merge_requests/<n> → GitLab), so github.com, gitlab.com and self-hosted instances of either all work. State is fetched through the provider’s REST API using a stored GITHUB / GITLAB credential — see Git links for how the credential is chosen and how private-network forges are allowed.
Response: 200 OK
state is OPEN, DRAFT, MERGED or CLOSED — normalised across both providers. A merged GitHub pull request reports closed with a merged_at; Crewship records it as MERGED.
Auth: OWNER, ADMIN, or MANAGER role Request Body:
Response: 201 Created — the stored link, the same object shape List code links returns.
The state is fetched before the row is written, so a 201 means the link was reachable. Errors are application/problem+json (RFC 7807) with a stable type URI under https://crewship.ai/problems/code-link/ and a code member, because several failures share a status:
Auth: OWNER, ADMIN, or MANAGER role Request Body: none. The link is addressed by the linkId path parameter; workspace_id is the only query parameter. Re-reads the link from its provider and returns the updated object. State is never polled in the background; this is the only refresh path. Response: 200 OK — the refreshed link, the same object shape List code links returns, with last_synced_at moved forward and last_sync_error cleared. A failed refresh keeps the previously fetched state and records the reason on the row (last_sync_error), then returns the same problem document the attach path would.
Auth: OWNER, ADMIN, or MANAGER role Request Body: none. The link is addressed by the linkId path parameter; workspace_id is the only query parameter. Response: 200 OK with {"status": "ok"} Hard delete — the row goes, the way a mission_relation does. Nothing about the pull request on the forge changes.

Attachments

Attach files to an issue — a crash log, a screenshot, a repro bundle, a diff. Everything an agent working the issue needs to see and, before this surface existed, had to be pasted into a comment. Three properties are worth knowing before you use it:
  • The type is decided by the file’s extension, against an allowlist. The request’s own Content-Type is discarded. That header is chosen by whoever is uploading, and honouring it is how a stored file becomes stored XSS served from your own origin. Allowed today: .txt .log .md .csv .tsv .json .yaml .yml .toml .xml .diff .patch .png .jpg .jpeg .gif .webp .avif .pdf .zip .gz .tgz. .html, .svg and anything executable are absent deliberately, not by oversight.
  • Files are stored by content. Two issues carrying identical bytes share one stored copy, and attaching the same file to the same issue twice is the same attachment — the second call returns 200 with the existing record rather than 201 or 409, so a retry is safe. “The same file” means the same bytes under the same name: crash-before-fix.log and crash-after-fix.log can be byte-identical and mean opposite things, so both are attached and both get 201, sharing one stored copy.
  • The uploaded filename is a label, never a path. The stored location is derived entirely from the SHA-256 digest, so a filename of ../../../etc/passwd is recorded as passwd and the bytes land where every other attachment’s do.
Agents reach the same files through the sidecar — see Attachments an agent can read.

List attachments

Auth: any workspace member (session or CLI token). The workspace is taken from the authenticated session, so an issue in another workspace is a 404. Request: workspace_id is the required workspace query parameter. No request body. Response: 200 OK — newest first.
sha256 is part of the contract: it lets a client verify what it downloaded, and lets an agent decide it has already read this exact file. The on-disk storage key is deliberately not exposed.

Upload an attachment

Auth: OWNER, ADMIN, or MANAGER role Request Body: multipart/form-data with one part.
Response: 201 Created — the stored record, the same object shape List attachments returns. 200 OK instead of 201 means this exact file — these bytes, under this filename — was already attached to this issue: no second record was created, no second timeline entry was written, and the returned object is the one that already existed. The same bytes under a different filename are a different attachment and get 201.

Download an attachment

Auth: any workspace member. Request: workspace_id is the required workspace query parameter. The file is addressed by the attachmentId path parameter. No request body. Response: 200 OK — the raw bytes, with three headers that matter: An attachment belonging to a different issue — even one in the same workspace — is a 404 through this issue’s URL, not a 403.

Delete an attachment

Auth: OWNER, ADMIN, or MANAGER role Request Body: none. The file is addressed by the attachmentId path parameter; workspace_id is the only query parameter. Response: 200 OK with {"status": "ok"} Hard delete of the record, and of the stored bytes — but the bytes only when no other issue, comment or chat in this workspace still references them. Removing a file from one issue never removes it from another that carries the same file.

Labels

Labels are workspace-scoped and can be attached to any issue.

List Labels

Request: workspace_id is the required workspace query parameter. No request body. Response: 200 OK

Create Label

Auth: OWNER, ADMIN, or MANAGER role Request: workspace_id selects the workspace. The JSON body requires name and color; label_group is optional. Response: 201 Created

Update Label

Auth: OWNER, ADMIN, or MANAGER role Request: labelId is required, workspace_id selects the workspace, and the JSON body may contain name, color, and/or label_group. All fields optional: name, color, label_group. Response: 200 OK — updated label.

Delete Label

Auth: OWNER or ADMIN role Request: labelId is a required path parameter and workspace_id selects the workspace. No request body. Response: 204 No Content

Projects

Projects group related issues together with tracking metadata.

List Projects

Query Parameters: Response: 200 OK

Create Project

Auth: OWNER, ADMIN, or MANAGER role Response: 201 Created

Get Project

Response: 200 OK — project with computed issue_count, done_count, and progress fields.

Update Project

Auth: OWNER, ADMIN, or MANAGER role All fields optional: name, description, icon, color, status, priority, health, lead_type, lead_id, start_date, target_date. Response: 200 OK — updated project.

Delete Project

Auth: OWNER or ADMIN role
Unlinks all issues from the project before deletion.
Response: 204 No Content

Project Stats

Returns breakdown data: counts by status, by assignee, by label, and crew list. Response: 200 OK

Milestones

Milestones belong to projects and expose progress counts for the issues linked to them. All routes are workspace-scoped through the project or milestone.

List Milestones

Request: projectId is a required path parameter and workspace_id selects the workspace. No request body. Returns 200 OK with an array ordered by position, then creation time. Each item contains id, project_id, name, nullable description and target_date, status, position, issue_count, done_count, created_at, and updated_at. A project outside the workspace returns 404.

Create Milestone

Auth: OWNER, ADMIN, or MANAGER role. The request requires name; optional fields are description, target_date, and status (default active). The response is 201 Created with the full milestone object and its next position.

Update Milestone

Auth: OWNER, ADMIN, or MANAGER role. Any of name, description, target_date, status, and position may be updated; at least one field is required. Returns 200 OK with the updated milestone, or 400 for invalid JSON/no fields and 404 when it is not in the workspace.

Delete Milestone

Auth: OWNER or ADMIN role. Returns 204 No Content, or 404 when the milestone is not in the workspace.

Reference

Issue Statuses

Priority Levels