Action Plans API
Base URL: {host}/api/v1
All responses wrapped in:
{
"statusCode": 200,
"message": "...",
"data": { ... },
"durationMs": 42,
"_reference": []
}
Shown data inline below for brevity.
Hierarchy & Member Counting
Activity
└─ Task (many)
└─ Subtask (many)
| Level | Has children? | Member count source |
|---|---|---|
| Activity | Tasks with subtasks | Union of all subtask members |
| Activity | Tasks without subtasks | Union of all task members |
| Activity | No tasks | Activity's own members |
| Task | Has subtasks | Union of all subtask members |
| Task | No subtasks | Task's own members |
| Subtask | — | Subtask's own members |
Example:
Activity1 (10 members) ← Union(Task1, Task2)
Task1 (5 members) ← Union(Subtask1, Subtask2)
Subtask1 (3 members)
Subtask2 (2 members)
Task2 (5 members) ← Task scope, no subtasks
Enums
Subtask/Task/Activity Status
| Value | Description |
|---|---|
INCOMPLETE_DATA |
Required fields missing |
NOT_STARTED |
Ready but no progress |
IN_PROGRESS |
Progress entries exist |
NEED_APPROVAL |
Pending progress requiring response |
COMPLETED |
100% complete |
Progress Entry Status
| Value | Description |
|---|---|
PENDING |
Submitted, awaiting response |
APPROVED |
Accepted |
APPROVED_WITH_NOTES |
Accepted with amendments (new version created) |
REJECTED |
Rejected |
ActionPlanMemberScope
| Value | Description |
|---|---|
ACTIVITY |
Member assigned to activity |
TASK |
Member assigned to task |
SUBTASK |
Member assigned to subtask |
Routes
1. Activities
Base: /api/projects/:projectId/action-plans
POST /
Create activity with optional nested tasks & subtasks.
Body:
{
"name": "Activity name", // string, 1-255
"tasks": [
// optional, max 50
{
"name": "Task name", // string, 1-255
"workWeight": 100, // optional, number >= 0
"workVolume": 500, // optional, number >= 0
"workVolumeUnit": "m2", // optional, string max 100
"startDate": "2026-01-01", // optional, ISO date
"endDate": "2026-03-31", // optional, ISO date
"subtasks": [
// optional, max 50
{
"name": "Subtask name", // string, 1-255
"workVolume": 250, // optional, number >= 0
"workVolumeUnit": "m2", // optional, string max 100
"workWeight": 50, // optional, number >= 0
"startDate": "2026-01-01", // optional, ISO date
"endDate": "2026-02-15", // optional, ISO date
},
],
},
],
}
Response: Full activity with derived stats (same shape as GET /:id).
GET /
List activities for a project. Paginated, searchable.
Query:
| Param | Type | Default | Description |
|---|---|---|---|
page |
number | 1 | Page number |
limit |
number | 20 | Items per page (max 100) |
search |
string | — | Filter by name |
sort_by |
created_at | updated_at | name |
created_at |
Sort field |
sort_order |
asc | desc |
desc |
Sort direction |
Response:
{
"items": [
{
"id": "uuid",
"projectId": "uuid",
"name": "Activity name",
"createdBy": "uuid",
"createdAt": "ISO date",
"updatedAt": "ISO date",
"deletedAt": null,
"taskCount": 2,
"subtaskCount": 5,
"workWeight": 150,
},
],
"total": 10,
"page": 1,
"limit": 20,
}
GET /tree
Full Activity > Task > Subtask tree. Each level has accumulated stats.
Query:
| Param | Type | Default | Description |
|---|---|---|---|
status |
string | — | Filter subtasks by status |
Response:
[
{
"id": "uuid",
"name": "Activity name",
"status": "IN_PROGRESS",
"totalTasks": 2,
"totalUsersAssigned": 10, // memberCount (bottom-up)
"accumulatedProgress": 45.5, // avg(), percent
"createdBy": "uuid",
"createdAt": "ISO date",
"updatedAt": "ISO date",
"tasks": [
{
"id": "uuid",
"name": "Task name",
"status": "IN_PROGRESS",
"totalSubtasks": 2,
"totalUsersAssigned": 5, // memberCount (bottom-up)
"accumulatedProgress": 60, // avg(), percent
"workWeight": 100,
"workVolume": 500,
"workVolumeUnit": "m2",
"startDate": "ISO date",
"endDate": "ISO date",
"subtasks": [
{
"id": "uuid",
"name": "Subtask name",
"status": "IN_PROGRESS",
"totalUsersAssigned": 3, // memberCount
"progress": 75, // effective (non-rejected), percent
"workVolume": 250,
"workVolumeUnit": "m2",
"workWeight": 50,
"startDate": "ISO date",
"endDate": "ISO date",
},
],
},
],
},
]
GET /tree/gantt
Same tree structure but each node includes startDate/endDate derived from children.
Activity startDate = min of all subtasks. Activity endDate = max of all subtasks.
Task dates use own stored values.
Supports status query filter (same as /tree).
GET /:id
Get single activity with derived stats.
Response:
{
"id": "uuid",
"projectId": "uuid",
"name": "Activity name",
"createdBy": "uuid",
"createdAt": "ISO date",
"updatedAt": "ISO date",
"deletedAt": null,
"taskCount": 2,
"subtaskCount": 5,
"memberCount": 10, // bottom-up (see rules above)
"status": "IN_PROGRESS",
"progress": 45.5, // avg(), percent
"workWeight": 150,
}
PATCH /:id
Update activity. If activity has tasks, only name is editable.
Body:
{
"name": "New name", // string, 1-255, optional
}
DELETE /:id
Soft-delete activity and all its tasks + subtasks. No body. Returns 204 status code.
2. Tasks
Base: /api/action-plans/activities/:activityId/tasks
POST /
Create task under an activity.
Body:
{
"name": "Task name", // string, 1-255
"workWeight": 100, // optional, number >= 0
"workVolume": 500, // optional, number >= 0
"workVolumeUnit": "m2", // optional, string max 100
"startDate": "2026-01-01", // optional, ISO date
"endDate": "2026-03-31", // optional, ISO date
}
Response: Task object with id, name, workWeight, workVolume, workVolumeUnit, startDate, endDate, createdAt.
GET /
List all tasks under an activity.
Response:
{
"tasks": [
{
"id": "uuid",
"name": "Task name",
"workWeight": 100,
"workVolume": 500,
"workVolumeUnit": "m2",
"startDate": "ISO date",
"endDate": "ISO date",
"createdAt": "ISO date",
"updatedAt": "ISO date",
"subtaskCount": 3,
},
],
}
GET /:id
Get task detail with derived stats.
Response:
{
"id": "uuid",
"activityId": "uuid",
"name": "Task name",
"workWeight": 150,
"workVolume": 500,
"workVolumeUnit": "m2",
"startDate": "ISO date",
"endDate": "ISO date",
"createdBy": "uuid",
"createdAt": "ISO date",
"updatedAt": "ISO date",
"subtaskCount": 3,
"memberCount": 5, // bottom-up (see rules above)
"status": "IN_PROGRESS",
"progress": 60, // avg(), percent
"canAddSubtask": false,
}
canAddSubtask is false if the task has subtasks and any subtask is beyond INCOMPLETE_DATA status.
PATCH /:id
Update task. If task has subtasks, only name is editable.
Body:
{
"name": "New name", // string, 1-255, optional
"workWeight": 200, // optional (only when no subtasks)
"workVolume": 600, // optional (only when no subtasks)
"workVolumeUnit": "km2", // optional (only when no subtasks)
"startDate": "2026-01-01", // optional (only when no subtasks)
"endDate": "2026-06-30", // optional (only when no subtasks)
}
DELETE /:id
Soft-delete task and all its subtasks. No body.
3. Subtasks
Base: /api/action-plans/tasks/:taskId/subtasks
POST /
Create subtask under a task.
Body:
{
"name": "Subtask name", // string, 1-255
"workVolume": 250, // optional, number >= 0
"workVolumeUnit": "m2", // optional, string max 100
"workWeight": 50, // optional, number >= 0
"startDate": "2026-01-01", // optional, ISO date
"endDate": "2026-02-15", // optional, ISO date
}
Response: Subtask object (see GET /:id).
GET /
List all subtasks under a task.
Response: Array of subtask objects, each with memberCount, members[].{userId, displayName}, status, progress, progressCount.
GET /:id
Get subtask detail.
Response:
{
"id": "uuid",
"taskId": "uuid",
"task": { "id": "uuid", "activityId": "uuid" },
"name": "Subtask name",
"workVolume": 250,
"workVolumeUnit": "m2",
"workWeight": 50,
"startDate": "ISO date",
"endDate": "ISO date",
"createdBy": "uuid",
"createdAt": "ISO date",
"updatedAt": "ISO date",
"deletedAt": null,
"members": [
{
"id": "uuid",
"userId": "uuid",
"displayName": "John Doe",
},
],
"memberCount": 3,
"status": "IN_PROGRESS",
"progress": 75,
"progressCount": 2,
"progressPlanCount": 1,
"plans": [
{
"id": "uuid",
"startDate": "ISO date",
"endDate": "ISO date",
"targetProgress": 50,
},
],
}
PATCH /:id
Update subtask. Work weight & work volume are locked once subtask is in progress.
Body:
{
"name": "New name", // string, 1-255, optional
"workVolume": 300, // optional (locked after progress starts)
"workVolumeUnit": "kg", // optional (locked after progress starts)
"workWeight": 60, // optional (locked after progress starts)
"startDate": "2026-01-01", // optional
"endDate": "2026-02-15", // optional
}
DELETE /:id
Soft-delete subtask. No body.
4. Progress
Three sets of progress endpoints — subtask, task, and activity scope.
Subtask Progress
Base: /api/action-plans/subtasks/:subtaskId/progress
POST / (multipart/form-data)
Submit progress entry. Supports file uploads.
Form fields:
| Field | Type | Description |
|---|---|---|
progress |
number (0-100) | New cumulative progress % |
workVolume |
number | Optional work volume completed |
description |
string | Optional note, max 5000 chars |
images |
file[] | Image attachments, max 10 |
sitemap |
file | Single PDF sitemap |
Progress auto-accumulates from previous APPROVED entries. If not provided, defaults to current effective progress.
When workVolume is provided with totalVolume, progress % is derived from workVolume ratio.
New progress must be >= current non-REJECTED floor.
Response: Full progress entry (see GET /:id).
GET /
List progress history for a subtask. Paginated, newest first.
Query: page (default 1), limit (default 50, max 100)
Response:
{
"items": [
{
"id": "uuid",
"subtaskId": "uuid",
"subtaskName": "Subtask name",
"progress": 75,
"workVolume": 187.5,
"subtaskWorkVolume": 250,
"subtaskWorkVolumeUnit": "m2",
"description": "Completed foundation",
"status": "APPROVED",
"amendsId": null,
"amendsOriginal": null,
"amendedByCount": 0,
"amendedBy": [],
"approvedBy": "uuid",
"approvedByName": "Admin",
"approvedAt": "ISO date",
"createdBy": "uuid",
"createdByName": "Worker",
"updatedBy": null,
"updatedByName": null,
"createdAt": "ISO date",
"updatedAt": "ISO date",
"images": [{ "id": "uuid", "key": "s3/path", "originalName": "photo.jpg", "url": "presigned-url" }],
"sitemap": { "id": "uuid", "key": "s3/path", "originalName": "site.pdf", "url": "presigned-url" },
},
],
"pagination": {
"page": 1,
"limit": 50,
"total": 2,
"pages": 1,
"hasNext": false,
"hasPrev": false,
},
}
GET /:id
Get single progress entry with full detail including images and amendment chain.
Response: Same shape as one item in the list above.
POST /:id/respond
Admin responds to a PENDING progress entry.
Body:
{
"verb": "APPROVE", // "APPROVE" | "APPROVE_WITH_NOTES" | "REJECT"
"progress": 80, // optional, 0-100 (for APPROVE_WITH_NOTES)
"workVolume": 200, // optional (for APPROVE_WITH_NOTES)
"description": "Adjusted", // optional (for APPROVE_WITH_NOTES)
}
Verbs:
APPROVE— progress accepted as-isAPPROVE_WITH_NOTES— original marked APPROVED_WITH_NOTES + new amended entry created with admin valuesREJECT— progress rejected, doesn't count toward effective progress
Task Progress
Base: /api/action-plans/tasks/:taskId
POST /progress
Create progress entry at task scope.
Body: Same as subtask progress body (minimal: { "progress": 25 }).
GET /progress
List progress entries. Query: page, limit.
GET /progress/:id
Get single progress entry.
POST /progress/:id/respond
Respond to PENDING progress entry. Body: Same respond schema.
Activity Progress
Base: /api/action-plans/activities/:activityId
POST /progress
GET /progress
GET /progress/:id
POST /progress/:id/respond
Same as task progress. Scope = ACTIVITY.
5. Progress Plans (S-Curve)
Set target progress over time. All levels (subtask/task/activity) share same shape.
Subtask Plans
Base: /api/action-plans/tasks/:taskId/subtasks/:id
GET /plans
Response:
{
"duration": {
"days": 90,
"startDate": "2026-01-01",
"endDate": "2026-03-31",
},
"planning": [
{
"id": "uuid",
"startDate": "2026-01-01",
"endDate": "2026-02-15",
"targetProgress": 50,
},
{
"id": "uuid",
"startDate": "2026-02-15",
"endDate": "2026-03-31",
"targetProgress": 50,
},
],
"sCurve": {
"plan": [
{ "date": 1704067200000, "progress": 0 },
{ "date": 1709251200000, "progress": 50 },
{ "date": 1711929600000, "progress": 100 },
],
"actual": [
{ "date": 1705276800000, "progress": 25, "id": "uuid" },
{ "date": 1708300800000, "progress": 60, "id": "uuid" },
],
},
}
sCurve.plan — cumulative target over time (derived from plan segments).
sCurve.actual — APPROVED entries sorted by approvedAt. Only latest version per amendment chain.
If no plans stored and subtask has dates, a default segment 0%→100% is returned.
PUT /plans
Set (replace) all plans. Validates: continuous dates, within subtask date range, sum to 100%.
Body:
{
"plans": [
{
"startDate": "2026-01-01",
"endDate": "2026-02-15",
"targetProgress": 50,
},
{
"startDate": "2026-02-15",
"endDate": "2026-03-31",
"targetProgress": 50,
},
],
}
Response: Same as GET /plans.
Task Plans
Base: /api/action-plans/tasks/:taskId
GET /plans
PUT /plans
Same shape as subtask plans. Scope = TASK.
Activity Plans
Base: /api/action-plans/activities/:activityId
GET /plans
PUT /plans
Same shape as subtask plans. Scope = ACTIVITY.
6. Members
Member CRUD across all three hierarchy levels. Uses unified actionPlanMembers table.
Subtask Members
Base: /api/action-plans/tasks/:taskId/subtasks/:id
POST /members
Add member to subtask. Also cascades to activity members (upsert).
Body:
{
"userId": "uuid",
}
Response:
{
"id": "uuid",
"scopeType": "SUBTASK",
"scopeId": "uuid",
"userId": "uuid",
}
GET /members
List members of a subtask.
Response:
[
{
"id": "uuid",
"userId": "uuid",
"displayName": "John Doe",
},
]
DELETE /members/:userId
Remove member from subtask. No body. Returns deleted member object.
Task Members
Base: /api/action-plans/tasks/:taskId
POST /members
GET /members
DELETE /members/:userId
Same pattern as subtask members. scopeType = TASK.
Activity Members
Base: /api/action-plans/activities/:activityId
POST /members
GET /members
DELETE /members/:userId
Same pattern as subtask members. scopeType = ACTIVITY.
7. Feedbacks
Discussion thread per progress entry. Sender can edit own message.
Base: /api/action-plans/progress/:progressId/feedbacks
POST /
Body:
{
"message": "Please clarify the numbers", // string, 1-5000
}
GET /
List feedbacks for a progress entry. Paginated.
Query: page, limit.
PATCH /:id
Edit own message. Body:
{
"message": "Updated message", // string, 1-5000
}
DELETE /:id
Delete own message. No body.
8. Analytics
Base: /api/projects/:projectId/action-plans/analytics
GET /
Query:
| Param | Type | Values | Default |
|---|---|---|---|
layout |
string | detailed | simplified |
detailed |
granularity |
string | daily | weekly | monthly |
daily |
search |
string | — | — |
startDate |
ISO date | — | — |
endDate |
ISO date | — | — |
9. Import
Base: /api/projects/:projectId/action-plans/import
GET /template/download
Download Excel template file. Returns .xlsx binary.
POST / (multipart/form-data)
Import activities from spreadsheet.
Form field:
| Field | Type | Description |
|---|---|---|
file |
file | .xlsx, .xls, or .csv, max 5MB |
Response:
{
"imported": 3,
"activities": [
{ "id": "uuid", "name": "Activity 1" },
{ "id": "uuid", "name": "Activity 2" },
],
}
Constraint Summary
| Constraint | Value |
|---|---|
| Max tasks per activity | 50 |
| Max subtasks per task | 50 |
| Max plans per scope | Unlimited |
| Max images per progress | 10 |
| Max feedback length | 5000 chars |
| Max description length | 5000 chars |
| Max import file size | 5MB |
| Progress range | 0–100 |
| Work weight/volume | Number >= 0 |
Editing Rules
- Activity with tasks → only
nameeditable - Task with subtasks → only
nameeditable - Subtask in progress →
workWeightandworkVolumelocked
Soft Delete
All deletes are soft (sets deletedAt). Cascades: deleting activity deletes all tasks + subtasks. Deleting task deletes all subtasks.