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 requireOWNER, ADMIN, or MANAGER role and emit a WebSocket event on workspace:{workspaceId}.
List Issues (Workspace)
Response:
200 OK
Response fields
Response fields
Get Issue (Workspace-scoped)
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
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:
201 Created — issue object with generated number and identifier.
WebSocket event:
issue.created on workspace:{workspaceId}
Get Issue (Crew-scoped)
crewId and identifier are required path parameters; workspace_id selects the workspace. No request body.
Response: 200 OK — full issue object.
Update Issue
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
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
BACKLOG or TODO issue. The issue must have an assignee. This:
- Creates a synthetic chat session for the mission
- Creates or resets tasks (resets existing tasks to PENDING for re-runs)
- Transitions status to
IN_PROGRESS - Starts the MissionEngine asynchronously
OWNER, ADMIN, or MANAGER role
Response: 200 OK
WebSocket event:
issue.started on workspace:{workspaceId}
Stop Issue
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
REVIEW or IN_PROGRESS status.
Auth: OWNER, ADMIN, or MANAGER role
Request Body:
approve: Transitions issue toDONE, adds approval comment and activityrequest_changes: Transitions issue toTODO, optionally reassigns, adds comment and activity
200 OK
Bulk
Apply one set of field updates across many issues from the board/list multi-select toolbar.Bulk Update
{"updated": 47} rather than rolling back.
Auth: OWNER, ADMIN, or MANAGER role (create permission).
Request Body:
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 viaparent_issue_id. List the children of one parent.
List Sub-issues
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
200 OK
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 anassignments row.
List Runs
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
crewId and identifier are required path parameters; workspace_id selects the workspace. No request body.
Response: 200 OK
Create Comment
OWNER, ADMIN, or MANAGER role
Request Body:
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
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
OWNER, ADMIN, or MANAGER role
Request Body:
blocked_by is stored as blocks with source/target swapped for normalization.
Response: 201 Created
Delete Relation
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"}
Code links
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.
List code links
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.
Attach a code link
OWNER, ADMIN, or MANAGER role
Request Body:
201 Created — the stored link, the same object shape
List code links returns.
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:
Refresh a code link
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.
Delete a code link
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-Typeis 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,.svgand 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
200with the existing record rather than201or409, so a retry is safe. “The same file” means the same bytes under the same name:crash-before-fix.logandcrash-after-fix.logcan be byte-identical and mean opposite things, so both are attached and both get201, 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/passwdis recorded aspasswdand the bytes land where every other attachment’s do.
List attachments
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
OWNER, ADMIN, or MANAGER role
Request Body: multipart/form-data with one part.
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
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
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
workspace_id is the required workspace query parameter. No request body.
Response: 200 OK
Create Label
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
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
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
Response:
200 OK
Create Project
OWNER, ADMIN, or MANAGER role
Response:
201 Created
Get Project
200 OK — project with computed issue_count, done_count, and progress fields.
Update Project
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
OWNER or ADMIN role
Response: 204 No Content
Project Stats
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
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
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
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
OWNER or ADMIN role. Returns 204 No Content, or 404 when the
milestone is not in the workspace.