Milestones
Milestones are first-class date-based checkpoints within a project. They represent key delivery dates, contractual deadlines, or payment gates that the team tracks independently of individual tasks.
Data Model
Each milestone belongs to a project and optionally to a stage. Key fields:
- title — human-readable name (required)
- dueDate — the deadline (required, ISO-8601 datetime)
- status — derived automatically:
PENDING,COMPLETED, orOVERDUE - completedAt — when set, status becomes
COMPLETED; when cleared, status reverts toPENDINGorOVERDUEbased ondueDate - stageId — optional association with a project stage
- paymentAmount / paymentCurrency / paymentStatus — optional payment metadata (no workflow enforced)
Status Derivation
Status is never set directly. It is computed from completedAt and dueDate:
| completedAt | dueDate vs now | Resulting status |
|---|---|---|
| non-null | any | COMPLETED |
| null | future | PENDING |
| null | past or now | OVERDUE |
Attempting to set status directly via PATCH returns HTTP 400.
API
All routes are project-scoped and require both:
- Project access — creator, active collaborator, or an internal-org member with org-wide
projects.read. - The corresponding
tasks.*permission viarequirePermission:tasks.readforGET /milestonesandGET /milestones/{milestoneId}tasks.createforPOST /milestonestasks.update.anyforPATCH /milestones/{milestoneId}tasks.delete.anyforDELETE /milestones/{milestoneId}
Dedicated milestones.* permission strings are tracked as a follow-up.
| Method | Path | Description |
|---|---|---|
GET | /api/projects/{id}/milestones | List milestones |
POST | /api/projects/{id}/milestones | Create milestone |
GET | /api/projects/{id}/milestones/{milestoneId} | Get milestone |
PATCH | /api/projects/{id}/milestones/{milestoneId} | Update milestone |
DELETE | /api/projects/{id}/milestones/{milestoneId} | Soft-delete |
Soft-deleted milestones are excluded from list and get responses.
Overdue Sweep (Cron)
A daily cron job (/api/cron/milestones/overdue, 02:00 UTC) transitions all past-due PENDING milestones to OVERDUE and publishes a projects.milestone.overdue domain event for each transitioned milestone.
The sweep is idempotent — running it multiple times produces no duplicate transitions or outbox rows.
Events
| Event | Trigger |
|---|---|
projects.milestone.overdue | Cron sweep transitions to OVERDUE |
See the events reference for the full payload schema.