Skip to main content

Work Requests API

Cross-team work requests raised on CRM records. RBAC module work_requests; every lifecycle action needs work_requests.edit at the guard, and the service then checks the caller's relationship to the request (requester, team member, assignee, or manager). Requests the caller may not view return 404. See Work Requests Module.

Endpoints​

MethodEndpointPermissionDescription
GET/work-requestsviewList requests (paginated)
GET/work-requests/countsviewBadge counts for the current user
GET/work-requests/by-entity/:entityType/:entityIdviewRequests raised on, or linked to, a record
POST/work-requestscreateRaise a request
GET/work-requests/:idviewRequest with time totals and allowed actions
PUT/work-requests/:ideditUpdate request details
DELETE/work-requests/:iddeleteSoft-delete (manager, or the requester while still unassigned and requested)
GET/work-requests/:id/stage-historyviewStage moves with time in stage
GET/work-requests/:id/activities?page=&limit=viewActivity timeline (limit ≤ 200, default 50)
GET/work-requests/:id/documentsviewFiles on the request
DELETE/work-requests/:id/documents/:documentIdeditRemove a file (uploader, assignee, requester or manager)

Upload files with the existing POST /upload/document/work_requests/:id.

Lifecycle​

MethodEndpointBodyAllowed when / for
POST/work-requests/:id/accept—requested and unassigned; owning-team member or manager
POST/work-requests/:id/assign{ userId, note? }Open; manager; target user active
POST/work-requests/:id/release{ reason? }Current assignee, accepted / in_progress → back to the team queue
POST/work-requests/:id/decline{ reason } (required)requested; owning-team member or manager
POST/work-requests/:id/start—accepted; assignee or manager → in_progress
POST/work-requests/:id/complete{ note? }accepted / in_progress; assignee or manager
POST/work-requests/:id/cancel{ reason? }Open; requester or manager
POST/work-requests/:id/reopen{ note? }declined / completed / cancelled; requester or manager
POST/work-requests/:id/change-stagesee belowaccepted / in_progress; assignee or manager

Managers are admins, users with work_requests record access all, the owning team's lead and the owning department's head. Every lifecycle endpoint returns the updated request (same shape as GET /work-requests/:id).

change-stage body​

{
"stageId": "uuid",
"note": "Demo scheduled for Thursday",
"assignmentMode": "user",
"assignToUserId": "uuid",
"assignToTeamId": null
}
FieldDescription
stageIdRequired. Active stage of the request's pipeline. Sequential pipelines allow only adjacent stages (cancel stages exempt).
assignmentModeOptional: stage_rule (default) · record_owner (current assignee) · user · team_lead. A manual pick that can't be honoured falls back to the stage rule.
assignToUserId / assignToTeamIdFor user / team_lead

Entering an is_won stage completes the request; an is_lost stage cancels it; an open stage moves an accepted request to in_progress and may hand it to the stage owner.

Query Parameters (GET /work-requests)​

ParamDescription
viewall (default) · assigned_to_me · requested_by_me · team_queue (unassigned requested requests for your teams) · my_teams
statusComma-separated statuses; also accepts open (requested, accepted, in_progress) and closed (declined, completed, cancelled)
typeId, teamId, pipelineId, stageIdFilters
assignedToUser id, or unassigned
requestedByUser id
entityType, entityIdPrimary record
prioritylow · medium · high · urgent
overduetrue — past due and still open
search≥ 2 chars; matches title, request number, description
sortBycreatedAt (default) · updatedAt · dueDate · priority · requestNumber · title · status · stage
sortOrderASC · DESC (default)
page, limitDefault 1 / 25; limit ≤ 200

Results are always filtered to requests the caller can view.

Response: { data: WorkRequest[], meta: { total, page, limit, totalPages } }.

Create Body​

{
"typeId": "uuid",
"title": "Product demo for Acme",
"description": "Focus on reporting and integrations",
"entityType": "opportunities",
"entityId": "uuid",
"priority": "high",
"dueDate": "2026-10-12T00:00:00Z",
"estimatedMinutes": 120,
"requestData": { "products_to_demo": ["Analytics"], "preferred_demo_date": "2026-10-10" },
"associations": [{ "type": "contacts", "id": "uuid", "label": "Attendee" }],
"assignedTo": "uuid",
"tags": ["enterprise"]
}
FieldNotes
typeId, titleRequired. Type must be active and have a pipeline with an open stage and an owning team or department.
entityType + entityIdOptional, together. Type must allow the record type; caller must be able to view the record.
requestDataAnswers keyed by the type's request field keys; required fields are enforced (400 with missingFields[]).
dueDateDefaults to now + the type's default_due_days
isBillable, hourlyRate, currencyDefault from the type
pipelineIdDefaults to the type's pipeline
teamIdUsed only when the type has no owning team
associations[]Extra records to link; failures are skipped
assignedToHonoured only when the caller can manage the request; otherwise the type's assignment mode applies
customFieldsJSONB

Update Body (PUT /work-requests/:id)​

Any of title, description, priority, dueDate, estimatedMinutes, requestData (re-validated), customFields (merged), tags, and — assignee or manager only — isBillable, hourlyRate, currency. Closed requests must be reopened first.

Work Request Object​

{
"id": "uuid",
"requestNumber": "WR-000042",
"title": "Product demo for Acme",
"description": "…",
"type": { "id": "uuid", "name": "Technical Demo", "color": "#7C3AED", "icon": null },
"pipeline": { "id": "uuid", "name": "Presales Demo" },
"stage": { "id": "uuid", "name": "Demo Prep", "color": "#3B82F6", "sortOrder": 2, "isWon": false, "isLost": false },
"stageEnteredAt": "2026-10-05T09:00:00Z",
"entity": { "type": "opportunities", "id": "uuid", "noun": "opportunity", "url": "/opportunities/uuid", "label": "Acme renewal" },
"team": { "id": "uuid", "name": "Presales" },
"department": { "id": "uuid", "name": "Solutions" },
"requestedBy": { "id": "uuid", "firstName": "Sam", "lastName": "Rep", "avatarUrl": null },
"assignedTo": { "id": "uuid", "firstName": "Pat", "lastName": "Engineer", "avatarUrl": null },
"assignedAt": "…",
"status": "in_progress",
"priority": "high",
"dueDate": "…",
"isOverdue": false,
"acceptedAt": "…", "startedAt": "…", "completedAt": null, "cancelledAt": null,
"declinedAt": null, "declinedBy": null, "declineReason": null,
"estimatedMinutes": 120,
"loggedMinutes": 45,
"isBillable": false, "hourlyRate": null, "currency": null,
"project": null,
"requestData": {}, "customFields": {}, "tags": [],
"createdBy": "uuid", "createdAt": "…", "updatedAt": "…"
}

GET /work-requests/:id (and every lifecycle response) adds:

{
"requestFields": [{ "key": "preferred_demo_date", "label": "Preferred demo date", "type": "date", "required": true }],
"time": { "totalMinutes": 45, "billableMinutes": 0, "billableAmount": 0 },
"can": {
"edit": true, "manage": false, "accept": false, "assign": false, "release": true,
"decline": false, "work": true, "cancel": false, "reopen": false, "logTime": true
}
}

Counts​

GET /work-requests/counts →

{ "assignedToMe": 3, "requestedByMe": 5, "teamQueue": 2, "overdue": 1 }

assignedToMe and overdue count your accepted / in_progress requests; requestedByMe counts your open requests; teamQueue counts unassigned requested requests for teams you belong to. The sidebar badge shows assignedToMe + teamQueue.

Stage History Entry​

{
"id": "uuid",
"fromStage": { "id": "uuid", "name": "New", "color": "#6366F1" },
"toStage": { "id": "uuid", "name": "Demo Prep", "color": "#3B82F6" },
"changedBy": { "id": "uuid", "firstName": "Pat", "lastName": "Engineer" },
"timeInStageSeconds": 86400,
"note": null,
"createdAt": "…"
}

The first entry (placement on the first stage at creation) has fromStage: null.