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)
The body stays a bare array. The total the filter matched — before
limit/offset — is in the X-Total-Count response header, with X-Limit and X-Offset echoing the values applied. An agent assignee carries assignee_slug next to assignee_name.
Response: 200 OK
Pagination (#2286): the body is still the bare array above — unchanged
for every existing client — but the response now also carries two headers:
A caller that only reads the array still gets identical bytes; a caller
that wants to know whether a 100-row page was the whole story reads the
headers instead of guessing.
crewship issue list prints a “Showing N of
TOTAL issues” footer from them and takes --offset to page further; the
issues board (hooks/use-issues-list.ts) uses them to drive its own
“Load more”.Error degradation contract (#2285): the issues board classifies a
failed request to this endpoint instead of rendering it as an empty
workspace. A
403 is shown separately from a 5xx/network failure — it’s
reported as “the request was refused”, naming a scoped agent/CLI token as
the likely cause (AuthKindCLIToken in internal/api/middleware.go is the
credential “an agent, CI job, or script holds”; a 403 here is what an
agent/CLI token whose scope doesn’t cover issues, or a transient token
problem, looks like) alongside a retry action, rather than presenting a
board with zero issues. See hooks/use-issues-list.ts for the
implementation and the full reasoning.Response fields
Response fields
execution.stage is the execution’s place in the work→Lead-review→correction loop:Only one execution per issue can be
working or reviewing at a time. The
narrative is in the guide: Work, hand-offs and Lead review.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.
Owner and delegate are independent (#2297, invariant “human owner, agent
delegate”): a
user write never touches delegate_agent_id and an agent
write never touches owner_user_id. The only way to empty a slot is the
explicit unassign, which empties both. The legacy assignee_type/assignee_id
pair is always written alongside as a compatibility projection of the last
write. The CLI: crewship issue update <id> --assignee <email|agent> --assignee-type user|agent.
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.
Work package B11 (
docs/prd/PRD-ISSUES-AND-ROUTINES-2026.md §10.4, #2368): the terminal-children rule.DONE or REVIEW is refused with a 409 while the issue has a non-terminal sub-issue (parent_issue_id pointing at this issue, status not yet DONE/CANCELLED/DUPLICATE) or a non-terminal plan task (mission_tasks row, status not yet COMPLETED/FAILED/CANCELLED/SKIPPED). The 409 body names every blocker. Add ?force=true to the query string to proceed anyway — the transition then commits and the status-change activity/event entry (both List Activity and List Events) records forced: true and the list of children that were still open, alongside the usual actor fields, as the receipt: no separate approval step, no new table, just the same audit row every other status change writes, with two more facts on it. A FAILED sub-issue still counts as a blocker — a failure is not the same thing as the work being finished.
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.Transfer Issue Work
OWNER, ADMIN, or MANAGER (create).
Request body:
200 OK — {"revision":4,"status":"TODO"}. A replay of the
same request returns {"revision":4,"replayed":true} without repeating its
effects. Human submit returns status REVIEW. Read the issue again for
work_mode, worker_user_id, worker_name, work_note, work_revision, and
work_stopping. While stopping is true, active runs have received cancellation
but have not yet finished; handing back or submitting must wait.
The work state, cancellation, comment and audit receipt commit together.
WebSocket events:
issue.updated; human handoff also emits inbox.updated.
CLI: crewship issue work.
Set Issue Review Policy
TODO or BACKLOG, with no assignment still
running.
Auth: authenticated workspace OWNER, ADMIN, or MANAGER (create).
Request body:
200 OK — {"revision":4,"client_review_required":true}. The
revision is the issue’s new work_revision; pass it to the next call that
pins one.
The CAS on
revision is what stops a stale browser from silently removing
acceptance that was added to newer work. WebSocket event: issue.updated.
CLI: crewship issue review-policy.
Start Issue
BACKLOG or TODO issue. The issue must have an agent
delegate — delegate_agent_id (or an assignee resolved from the legacy
assignee_type='agent' pair), naming a live, non-held agent. A human
owner with no delegate is not enough (F62: existence of any assignee
used to be sufficient; it is not anymore — the delegate must specifically be
an executable agent). This:
Start takes one of two paths, chosen by whether the issue has a bound routine
(routine_id):
Mission path (no routine bound). This:
- Creates a synthetic chat session for the mission
- Creates or resets unfinished tasks to PENDING; completed and skipped history stays intact
- Transitions status to
IN_PROGRESS - Starts the MissionEngine asynchronously
routine_id set, #2448). The bound routine runs instead of
the mission engine, under the caller’s own identity and the routine’s public
execution gates (the same checks as running the routine directly). The
server records an issue_executions row in stage working for the Lead to
review, moves the issue to IN_PROGRESS, and answers 202 with the run ID.
Stored routine_inputs_json are the run’s inputs; a required input with no
value and no default is refused before anything is written. A routine start
the routine gate rejects is proxied back with that gate’s status and body,
and the execution is marked failed. Missing delegate is not a condition on
this path.
Auth: OWNER, ADMIN, or MANAGER role
Response (mission path): 200 OK
202 Accepted
run_id is the routine run to follow (crewship routine records); it is
also the execution.routine_run_id on Get Issue.
WebSocket event:
issue.started on workspace:{workspaceId}
Stop Issue
crewId and identifier are required path parameters; workspace_id selects the workspace; hard (optional, default false) opts into Tier 2. No request body.
Auth: OWNER, ADMIN, or MANAGER role
Response: 200 OK
status is CANCELLED when the issue was IN_PROGRESS/REVIEW; otherwise
it echoes the issue’s unchanged status (e.g. BACKLOG) for the
mention-dispatched-run case above. runs_stopped is the number of
assignment rows stamped by Tier 1. hard echoes whether ?hard=true was
requested — it does not report per-target Tier 2 results, which live on
each assignment row and in the journal instead.
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, and moves a Lead-accepted(orneeds_human) execution tochanges_requestedwith the comment as itsnote
409:
Response:
200 OK
CLI:
crewship issue review sends both
revisions from the issue it just read; --revision/--brief-revision pin them.
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, commented, mentioned (an @mention in a comment was recorded for dispatch — the row is the only user-visible trace of the delivery besides the inbox item), attachment_added, attachment_removed, code_link_added, code_link_removed, review_approved, review_changes_requested, task_completed, task_failed, task_cancelled (Stop reached a task or a live run), inbox_acted (a person acted on the issue’s NEEDS_HUMAN inbox card — the receipt names the action, the card, the session’s agent_version and, for an answer, the delivery and run it produced; see Acting on a card)
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.
List Events
Work package B11 of the Issues & Routines architecture (
docs/prd/PRD-ISSUES-AND-ROUTINES-2026.md §14.1/§17, #2368). This is the read side of the B1 event log (mission_activity, work package B1) — the SAME rows List Activity above returns, ordered by seq (not created_at) and not capped at 50.hooks/use-realtime.tsx) only guards which event TYPES a client accepts — it says nothing about whether every frame of an accepted type actually arrived. The server’s WebSocket hub sends non-blocking and silently drops a frame when a client’s send buffer is full. issue.delivery.acked (work package B2) is the one realtime type today whose payload carries a seq; a client that notices its next seq isn’t exactly one more than the last calls this endpoint with after_seq set to the last seq it has confirmed, and gets back everything in between — instead of staying stale until someone reloads the tab.
The path’s
{identifier} also accepts the issue’s id, as does GET /api/v1/issues/{identifier}, for issues known only by id.
Response: 200 OK
payload_json carries the same action/from/to shape the journal entry gets, plus forced/open_children on a status change that bypassed the terminal-children rule with ?force=true — the receipt naming which children were still open. It is null on a row from before this column existed.
Auth: any authenticated workspace member (read).
Get Full Run Result
runId is the assignment id from
List Runs, not its trace ID. The assignment must belong to the supplied issue.
The list exposes result_truncated for a shortened preview and result_stale
when its recorded brief revision predates the current issue brief.
Auth: any authenticated workspace member (read). No request body.
Query parameter: workspace_id selects the authenticated workspace.
Response: 200 OK
result_summary is the stored agent-authored text and may be empty. The
endpoint does not invent a deliverable or confirm acceptance of the issue.
CLI:
crewship issue result.
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, but also mention-dispatched runs (an agent tagged in a comment) and delegation-hop runs (a sub-agent assigned further work via /assign mid-mission). Newest-first.
Pages like every list: limit (default 100, max 500) and offset, with the total in the X-Total-Count, X-Limit and X-Offset response headers. Each row is one assignment (an agent task-run) and carries:
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).
hard_stop_result/hard_stop_at are omitted here — this run was never
hard-stopped. A run Tier 2 reached instead carries them, e.g.
"hard_stop_result": "TERMINATED_TERM", "hard_stop_at": "2026-09-04T10:00:05Z".
Outcome
Work package B6 of the Issues & Routines architecture (
docs/prd/PRD-ISSUES-AND-ROUTINES-2026.md §9.6). status (above), outcome and runverdict answer three different questions and are easy to conflate — see docs/guides/routines.mdx’s “Status, outcome and verdict” section for the full comparison.pipeline_runs) run — carries an outcome, the deterministic routing decision the runner reports, distinct from its technical status:
What this table describes vs. what is wired today.
NEEDS_HUMAN’s inbox
routing and the session-state transition below are real, outcome-driven
effects — this PR (B6) added them. The ”+ an issue comment” cells for
WORK_CREATED/PARTIAL are §9.6’s stated intent, not yet a real,
outcome-gated effect: an issue-session assignment already gets a mission
comment posted on completion (finishAssignment), but that path predates
outcome and fires for ANY non-empty, non-error result — a SUCCEEDED or
NO_CHANGE run with output gets one too, and a routine (pipeline_runs)
run gets no issue comment at all regardless of outcome, since no
comment-posting path exists for that run shape yet. Gating the comment on
outcome specifically (and adding one for routine runs) is a follow-up, not
part of this PR.outcome is parsed from the agent’s existing structured hand-off — the ---CHECKPOINT--- block a session-bearing run is instructed to emit (§11.3), or the ---HANDOFF--- block a mission-task run emits — never a second, invented block. A run that ends cleanly (no execution error) but reports no recognised outcome is recorded FAILED, with error_message set to "no outcome reported" — an absent outcome is treated as a bug, not a silent success. That default applies wherever somebody could have reported one: an agent run always could, and so could a routine holding an agent_run (or a call_pipeline, whose target may hold one). A routine with no agent step at all — an agentless: true probe, or one built only from script / transform / notify / crewship steps — could never report one, so a clean run there is recorded SUCCEEDED with an empty error_message rather than being flagged for a hand-off that was impossible by construction. A run stopped by Stop (Tier 1 cooperative cancellation) or superseded always gets CANCELLED, regardless of what its own hand-off said.
NEEDS_HUMAN is the only outcome that creates an inbox item — exactly one, keyed on the run id, so a retried terminal write can never double it. The item’s payload_json carries a §12 action contract:
NEEDS_HUMAN also moves the issue’s session to awaiting_input (see Sessions, below) — the transition that needed this column to exist before it could be reached at all.
WebSocket event: run.outcome on workspace:{workspaceId} — {mission_id, identifier, assignment_id, status, outcome} (work package B11, #2368). Fired once the outcome above is decided, for a run attributed to an issue; not fired for a run with no mission_id (a root /assign).
Sessions
Session lifecycle:
issue_agent_sessions is work package B1 (event log + session foundations) of the Issues & Routines architecture (docs/prd/PRD-ISSUES-AND-ROUTINES-2026.md), extended by B3 (#2339, the run-admission guarantee: at most one assignments row may be PENDING/QUEUED/RUNNING for a given session at a time, a partial unique index rather than a read-then-write check), B4 (#2343) and B6 (#2349, the outcome contract): state genuinely moves with its runs — pending → active when a run wins the admission CAS, then, once that run settles, resolved through its outcome — active → idle for NO_CHANGE/SUCCEEDED/WORK_CREATED/PARTIAL/CANCELLED, active → error for FAILED, and active → awaiting_input for NEEDS_HUMAN (the transition B4 could not reach until outcome existed) — with active_run_id populated for as long as a run is live, cleared once it settles. What is still NOT wired: the 14-day idle → stale sweep. An ephemeral agent that expires mid-session is reconciled onto error (was active) or closed (otherwise) rather than left rendered active over a ghost.UNIQUE(mission_id, agent_id) on the underlying table means mentioning the same agent on the same issue twice never creates a second session — the second mention finds and reuses the first’s row. UNIQUE on assignments.session_id (partial, live statuses only) means a session backs at most one live run: a mention that lands while the session’s run is already going does not start a second one — its delivery is left unclaimed and is folded into exactly one new run once the live one finishes, rather than being marked seen by a run that never read it. See docs/guides/issue-mentions.mdx’s “Known limits in 1.0” for the full account.
WebSocket event: issue.session.state on workspace:{workspaceId} — {mission_id, identifier, session_id, agent_id, state} (work package B11, #2368). Fired on every landed transition above; not fired for a transition the row’s own guard refused (e.g. activating a closed session).
List Sessions
issue_agent_sessions row for this issue, newest-updated first.
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).
List Checkpoints
Work package B5 of the Issues & Routines architecture (
docs/prd/PRD-ISSUES-AND-ROUTINES-2026.md §9.5, #2345). A checkpoint is the structured state a session-bearing run reports at the end of every run (done/plan/facts/blockers/next_step/confidence), so the next run on the same session can resume without rediscovering it. It is what the §11.1 context pack’s “latest checkpoint” section is built from — see Sessions, checkpoints and the context pack.agent_session_checkpoints row for one session, newest-first — the session’s full resumable history, not just its current cursor (GET .../sessions already reports last_consumed_seq).
WebSocket event: issue.checkpoint.written on workspace:{workspaceId} — {mission_id, identifier, session_id, agent_id} (work package B11, #2368). Fired when a new row lands here, so an open session panel can refresh without polling.
Request: crewId, identifier and sessionId are required path parameters; workspace_id selects the workspace. sessionId must belong to the named issue, or the request 404s. 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
page_size return all comments. Set page_size=100 for the latest page
(maximum 100) and pass its oldest comment ID as before_id to read older
messages. X-Has-More: true indicates another page. The stable timestamp + ID
cursor is not shifted by a newly posted comment.
Request: 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.