Projects API
The Projects API covers the full project lifecycle: listing and creating projects, reading a project and its details, updating fields, statuses, budgets, and colours, working with milestones and the project history feed, creating and scheduling tasks inside a project, and deleting a project with all of its related data.
Authentication: every endpoint requires an API key. See
API Keys & Authentication. All examples use
the Authorization: Bearer rnk_your_key header.
All requests run inside the organisation your key belongs to. Successful
responses use the standard envelope described in the
REST API Overview, for example
{ "success": true, "project": { ... } }. Validation failures return 400
with details of the invalid fields.
Field reference
Section titled “Field reference”These values appear throughout this page:
| Field | Valid values |
|---|---|
Project status | draft, active, review_internal, client_review, done, on_hold, completed, cancelled, archived (read only in generic updates, see Archiving a project) |
Project stage | discovery, planning, design, development, testing, review, deployment, maintenance |
Project workflowStage.category | draft, active, on_hold, done, cancelled |
Project color | guava, mandarin, matcha, acai, coffee, guava-dark, mandarin-dark, matcha-dark, acai-dark, coffee-dark |
Task priority | low, medium, high, critical |
Task status | pending, scheduled, in_progress, completed, cancelled |
Dates are ISO 8601 strings. IDs are UUIDs.
Project responses also carry workflowStageId and a workflowStage object
(id, name, category): the project’s Kanban workflow stage. stage is
the separate legacy lifecycle field and is unrelated to boards.
Archived projects additionally carry archivedAt, archivedBy (null when
archived automatically), preArchiveStatus, and preArchiveWorkflowStageId.
These are null on every other project.
GET /api/v1/projects
Section titled “GET /api/v1/projects”List the projects in your organisation, newest first, with pagination.
Any organisation member can call this. Admins and owners see every project. Other members see internal projects, projects for clients whose staffing list includes them (or uses the all-staff default), and any project where they’re an explicit project member.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| status | string | No | Filter by project status. See the field reference above. |
| stage | string | No | Filter by project stage. See the field reference above. |
| limit | integer | No | Page size, 1 to 100. Defaults to 50. |
| offset | integer | No | Number of projects to skip. Defaults to 0. |
| includeArchived | boolean | No | Archived projects are excluded by default. Set true to include them, or filter with status=archived to list only archived projects. |
Example request
curl 'https://api.runnit.io/api/v1/projects?status=active&limit=20' \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true, "projects": [ { "id": "1f0e9a52-...", "organizationId": "8c2d41b7-...", "jobNumber": "RB-2026-014", "name": "Winter Launch", "description": "Delivery workspace for the winter campaign.", "status": "active", "stage": "planning", "ownerId": "d3a6f0c1-...", "budget": 75000, "budgetCurrency": "AUD", "startDate": "2026-07-01T00:00:00.000Z", "targetCompletionDate": "2026-08-31T00:00:00.000Z", "clientOrganizationId": "b91c77e4-...", "color": "matcha", "tags": [], "createdAt": "2026-06-20T02:14:09.000Z", "updatedAt": "2026-07-01T04:33:52.000Z" } ], "pagination": { "total": 42, "limit": 20, "offset": 0, "hasMore": true }}budget, startDate, targetCompletionDate, clientOrganizationId,
color, summary, and description can be null or absent. Projects also
carry other metadata fields not shown here.
Errors
400if no active organisation can be resolved for your account, or a query parameter is invalid.
POST /api/v1/projects
Section titled “POST /api/v1/projects”Create a project directly, without the Brief Builder workflow. The project is
created in draft status with you as the owner.
You need project creation access, and your organisation must have direct project creation enabled in its settings.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Project name. Must not be empty. |
| description | string | No | Longer description of the work. |
| clientOrganizationId | UUID | No | Client the project is for. Omit for an internal project. |
| budget | number | No | Project budget amount. |
| budgetCurrency | string | No | Currency code. Defaults to your organisation’s currency, then AUD. |
| startDate | ISO 8601 date | No | Planned start date. |
| targetCompletionDate | ISO 8601 date | No | Target delivery date. |
| jobNumber | string | No | 1 to 100 characters. Runnit generates one if omitted. |
| color | string | No | Project colour key. See the field reference above. A random colour is used if omitted. |
| workflowStageId | UUID | No | Entry workflow stage, for example a sales pipeline’s Enquiry stage. Must be a draft-category stage in your organisation (400 otherwise); the project then appears on every board whose columns map that stage. Omit for the standard Planning (Draft) stage. |
Example request
curl -X POST https://api.runnit.io/api/v1/projects \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "name": "Winter Launch", "description": "Delivery workspace for the winter campaign.", "clientOrganizationId": "<client-uuid>", "budget": 75000, "budgetCurrency": "AUD", "startDate": "2026-07-01", "targetCompletionDate": "2026-08-31" }'Response
Returns 201 with the created project:
{ "success": true, "project": { "id": "1f0e9a52-...", "name": "Winter Launch", "jobNumber": "RB-2026-014", "status": "draft", "budget": 75000, "budgetCurrency": "AUD" }}Errors
400ifnameis missing or a field fails validation.400if no active organisation can be resolved for your account.403with codeDIRECT_PROJECT_CREATION_DISABLEDif the organisation has not enabled direct project creation.404if the organisation record cannot be found.
GET /api/v1/projects/:id
Section titled “GET /api/v1/projects/:id”Get a single project by ID.
Any member of the project’s organisation can call this.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Project ID. |
Example request
curl https://api.runnit.io/api/v1/projects/<project-id> \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true, "project": { "id": "1f0e9a52-...", "name": "Winter Launch", "status": "active" } }The project object has the same shape as in the list response.
Errors
404if the project does not exist.403if the project belongs to an organisation you are not a member of.
GET /api/v1/projects/:id/details
Section titled “GET /api/v1/projects/:id/details”Get a project together with its team members, tasks, milestones, and brief in one call.
Any member of the project’s organisation can call this.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Project ID. |
Example request
curl https://api.runnit.io/api/v1/projects/<project-id>/details \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true, "project": { "id": "1f0e9a52-...", "name": "Winter Launch", "status": "active" }, "members": [ { "id": "77a1c9d0-...", "userId": "d3a6f0c1-...", "isAdmin": true, "roleTitle": "Creative Director", "user": { "id": "d3a6f0c1-...", "name": "Alex Chen", "email": "alex@example.com" } } ], "tasks": [ { "id": "4be2d871-...", "name": "Draft homepage copy", "status": "scheduled", "priority": "high", "assignedToId": "d3a6f0c1-...", "estimatedHours": 6, "plannedStartDate": "2026-07-13T00:00:00.000Z", "plannedEndDate": "2026-07-14T00:00:00.000Z" } ], "milestones": [ { "id": "0a4f6c33-...", "name": "Client sign-off", "type": "custom", "targetDate": "2026-08-15T00:00:00.000Z", "isFirm": true, "orderIndex": 0 } ], "brief": { "...": "..." }}brief is null or absent for projects created without the Brief Builder.
Member, task, and milestone objects include further metadata fields.
Errors
404if the project does not exist.403if the project belongs to an organisation you are not a member of.
GET /api/v1/projects/:id/history
Section titled “GET /api/v1/projects/:id/history”Get the project’s history feed: status changes, brief edits, task activity, comments, milestone changes, and more.
Any member of the project’s organisation can call this.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Project ID. |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| limit | integer | No | Page size, 1 to 200. Defaults to 100. |
| offset | integer | No | Number of events to skip. Defaults to 0. |
| q | string | No | Free-text search across event titles and descriptions. |
| eventTypes | string | No | Comma-separated list of event types to include, for example task.created,task.status.updated. |
| categories | string | No | Comma-separated list of categories. Valid values: project, brief, task, comment, asset, team. Unknown values are ignored. |
| taskId | UUID | No | Only return events linked to this task. |
| significantOnly | boolean | No | When true, excludes routine automatic events (AI column fills and dependency cascade reschedules). The in-app dashboard uses this; the full feed includes them. |
Example request
curl 'https://api.runnit.io/api/v1/projects/<project-id>/history?categories=task,comment&limit=50' \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true, "events": [ { "id": "9d21e07b-...", "projectId": "1f0e9a52-...", "taskId": "4be2d871-...", "actorUserId": "d3a6f0c1-...", "actor": { "id": "d3a6f0c1-...", "name": "Alex Chen", "email": "alex@example.com" }, "category": "task", "eventType": "task.status.updated", "title": "Task status updated: Draft homepage copy", "description": "Status changed from scheduled to in_progress.", "metadata": { "previousStatus": "scheduled", "nextStatus": "in_progress" }, "createdAt": "2026-07-06T23:41:12.000Z" } ], "filters": { "eventTypes": ["task.created", "task.status.updated", "project.updated"], "categories": ["project", "task", "comment"] }, "pagination": { "total": 128, "limit": 50, "offset": 0, "hasMore": true }}taskId, actorUserId, actor, and description can be null. filters
lists the event types and categories that actually appear in this project’s
history, which is useful for building filter controls.
Errors
404if the project does not exist.403if the project belongs to an organisation you are not a member of.
PATCH /api/v1/projects/:id
Section titled “PATCH /api/v1/projects/:id”Update a project’s common editable fields in one call. Send only the fields you want to change.
Requires the project owner, a project admin, or an organisation admin or owner.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Project ID. |
Request body (all fields optional, at least one required)
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | No | New project name. Must not be empty. |
| description | string or null | No | Project description. Null clears it. |
| summary | string or null | No | Project summary. Null clears it. |
| status | string | No | New status. archived is not accepted here: use POST /:id/archive. See the field reference above. |
| jobNumber | string | No | 1 to 100 characters. |
| startDate | ISO 8601 date or null | No | Planned start date. Null clears it. |
| targetCompletionDate | ISO 8601 date or null | No | Target delivery date. Null clears it. |
| budget | number or null | No | Budget amount, zero or greater. Null clears it. |
| budgetCurrency | string | No | Exactly 3 letters, for example AUD. Stored uppercase. |
| clientOrganizationId | UUID or null | No | Client to link. Must be an active client of the organisation, otherwise 400. Null removes the client link and makes the project internal. Changing the client clears any pinned client rate card. |
| color | string | No | Project colour key. See the field reference above. |
| tags | array of strings | No | Replaces the full tag list. |
Example request
curl -X PATCH https://api.runnit.io/api/v1/projects/<project-id> \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "status": "active", "targetCompletionDate": "2026-09-30", "budget": 80000, "budgetCurrency": "AUD" }'Response
{ "success": true, "project": { "id": "1f0e9a52-...", "status": "active", "budget": 80000 } }Returns the full updated project. If the status changed to a completed state, Runnit also records the work in each contributor’s portfolio history.
Errors
400if no updatable fields are supplied (“No updates provided”).400ifjobNumberis supplied but empty after trimming.404if the project does not exist.403if you are not a member of the project’s organisation, or you are not the project owner, a project admin, or an organisation admin or owner.409with codeRATE_CARD_CONFIRMATION_REQUIREDorPROJECT_BUDGET_REQUIREDwhenstatuswould activate a draft that still needs its rate card confirmed or a required budget set. The same activation checks as the status endpoint apply, and no other fields are changed when the check fails.409with codePROJECT_INCOMPLETE_TASKSwhenstatusis a terminal value (done,completed,cancelled) and incomplete tasks remain. See Incomplete task checks.409with codePROJECT_ARCHIVEDif the project is archived. Unarchive it first.
PATCH /api/v1/projects/:id/brief
Section titled “PATCH /api/v1/projects/:id/brief”Update the project’s brief content (description and summary).
Only the project owner can call this.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Project ID. |
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| description | string | No | Brief description text. |
| summary | string | No | Brief summary text. |
Example request
curl -X PATCH https://api.runnit.io/api/v1/projects/<project-id>/brief \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "summary": "Repositioned for a spring release window." }'Response
{ "success": true, "project": { "id": "1f0e9a52-...", "summary": "Repositioned for a spring release window." } }Errors
404if the project does not exist.403if you are not a member of the project’s organisation.403if you are not the project owner (“Only project owner can update the brief”).
PATCH /api/v1/projects/:id/status
Section titled “PATCH /api/v1/projects/:id/status”Update only the project’s status.
Organisation admins and owners can always call this. Other members can update the status of internal projects, projects they are a team member of, and client projects for clients they can access, unless their per-member permission to update project statuses has been switched off.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Project ID. |
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| status | string | Yes | New status. archived is not accepted here: use POST /:id/archive. |
| acknowledgeIncompleteTasks | boolean | No | Confirms a terminal status change that would leave incomplete tasks behind. See Incomplete task checks. |
| incompleteTaskHandling | string | No | leave, complete_remaining, or cancel_remaining. |
Example request
curl -X PATCH https://api.runnit.io/api/v1/projects/<project-id>/status \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "status": "completed" }'Response
{ "success": true, "project": { "id": "1f0e9a52-...", "status": "completed" } }Moving a project to a completed status also records the work in each contributor’s portfolio history.
Schedule bookings follow the status change. Moving a draft project to an
active or on-hold status confirms all schedule entries for its tasks. Moving
a project back to draft makes those reservations tentative again, except
entries that were individually held. Moving a draft straight to done or
cancelled leaves its tentative reservations unchanged.
The status also keeps the project’s Kanban workflow stage in step: the project moves to the stage that matches the new status. A status update that matches the project’s current stage leaves a custom stage in place.
Errors
400ifstatusis missing, not a valid value, orarchived.409with codeRATE_CARD_CONFIRMATION_REQUIREDwhen an AI-created draft requires its project rate card to be confirmed before activation. Select the card through the organisation project-rate-card endpoint, then retry.409with codePROJECT_BUDGET_REQUIREDwhen the draft’s planning policy requires a budget and none is set.409with codePROJECT_INCOMPLETE_TASKSwhen moving to a terminal status while incomplete tasks remain. See Incomplete task checks.409with codeUSE_ARCHIVE_ENDPOINTif the project is archived. Status changes on archived projects go through POST /:id/unarchive.404if the project does not exist.403if you are not a member of the project’s organisation, your status permission is switched off, or the project is for a client you cannot access.
PATCH /api/v1/projects/:id/stage
Section titled “PATCH /api/v1/projects/:id/stage”Move a project to a workflow stage (the same action as dragging its card on a Kanban board). The project’s status syncs to the stage automatically, with the same permission rules, activation checks, and schedule booking behaviour as the status endpoint.
Moving a project onto a stage whose status is archived archives it: this
requires the archive permission (project:archive) and behaves exactly like
POST /:id/archive, including the eligibility
rule and the incomplete task check. Moving a project out of an archived stage
is rejected; unarchive it instead.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Project ID. |
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| workflowStageId | UUID | Yes | Target workflow stage. List stages with the organisation workflow-stages endpoint. |
| acknowledgeIncompleteTasks | boolean | No | See Incomplete task checks. |
| incompleteTaskHandling | string | No | leave, complete_remaining, or cancel_remaining. |
Example request
curl -X PATCH https://api.runnit.io/api/v1/projects/<project-id>/stage \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "workflowStageId": "<stage-id>" }'Response
{ "success": true, "project": { "id": "1f0e9a52-...", "status": "active", "workflowStageId": "<stage-id>" } }Errors
400ifworkflowStageIdis missing or not a UUID.404if the project or the stage does not exist in your organisation.409with codeRATE_CARD_CONFIRMATION_REQUIREDorPROJECT_BUDGET_REQUIREDwhen moving a draft into an active-category stage and the activation checks fail.409with codesPROJECT_INCOMPLETE_TASKS,PROJECT_NOT_ARCHIVABLE, orUSE_ARCHIVE_ENDPOINTunder the archive rules described above.403under the same permission rules as the status endpoint, or without the archive permission when the target stage is archived.
POST /api/v1/projects/:id/archive
Section titled “POST /api/v1/projects/:id/archive”Archive a project. Archiving hides the project from project lists, boards, and pickers, and makes it fully read-only until it’s unarchived. The project’s current status and workflow stage are saved so unarchiving can restore it exactly where it left off.
Requires the archive permission (project:archive). By default only
organisation admins and owners have it; it can be granted to other roles in
the permissions matrix.
Only done, completed, or cancelled projects can be archived. Complete
or cancel the project first.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Project ID. |
Request body (optional)
| Name | Type | Required | Description |
|---|---|---|---|
| acknowledgeIncompleteTasks | boolean | No | Confirms archiving even though incomplete tasks remain. |
| incompleteTaskHandling | string | No | leave, complete_remaining, or cancel_remaining. |
Example request
curl -X POST https://api.runnit.io/api/v1/projects/<project-id>/archive \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "incompleteTaskHandling": "complete_remaining" }'Response
{ "success": true, "project": { "id": "1f0e9a52-...", "status": "archived", "archivedAt": "2026-07-18T04:12:00.000Z", "preArchiveStatus": "completed" } }Archiving raises a project.archived event for automations, webhooks, and
notifications. The check for incomplete tasks always runs when archiving,
whatever the project’s previous status.
Errors
403without the archive permission.404if the project does not exist.409with codePROJECT_NOT_ARCHIVABLEif the project is notdone,completed, orcancelled.409with codePROJECT_INCOMPLETE_TASKSwhen incomplete tasks remain. See Incomplete task checks.
POST /api/v1/projects/:id/unarchive
Section titled “POST /api/v1/projects/:id/unarchive”Restore an archived project. The project returns to the status and workflow stage it had when it was archived. If that stage no longer exists, it falls back to the standard stage for its previous status.
Requires the same archive permission as archiving.
Example request
curl -X POST https://api.runnit.io/api/v1/projects/<project-id>/unarchive \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true, "project": { "id": "1f0e9a52-...", "status": "completed", "archivedAt": null } }Unarchiving raises a project.unarchived event.
Errors
403without the archive permission.404if the project does not exist.409with codePROJECT_NOT_ARCHIVEDif the project is not archived.
Archived projects are read only
Section titled “Archived projects are read only”While a project is archived, every write to it or anything inside it is
rejected with 409 and code PROJECT_ARCHIVED. That includes project
fields, budget, colour, brief, milestones, team membership, tasks and their
dates, estimates, allocations, dependencies, assignees and table values,
project views, comments on its conversations, and attaching new assets.
Reads keep working, and deleting an archived project is still allowed.
Incomplete task checks
Section titled “Incomplete task checks”When a project moves to a terminal status (done, completed,
cancelled), or is archived from any status, Runnit checks for tasks that
are not yet completed or cancelled. The organisation’s project lifecycle
settings control what happens:
-
Warn (default): the request fails with
409, codePROJECT_INCOMPLETE_TASKS, and adetailsobject:{"error": "This project still has 3 incomplete task(s).","code": "PROJECT_INCOMPLETE_TASKS","details": {"incompleteTaskCount": 3,"byStatus": { "pending": 2, "in_progress": 1 },"policy": "warn","targetStatus": "completed"}}Retry with
acknowledgeIncompleteTasks: true(orincompleteTaskHandling: "leave") to proceed and leave the tasks as they are, or sendincompleteTaskHandlingset tocomplete_remainingorcancel_remainingto resolve them as part of the same change. -
Block: only
complete_remainingorcancel_remainingis accepted. Acknowledging or leaving tasks unresolved is rejected. -
Off: no check runs.
complete_remaining marks every remaining task completed (progress 100%).
cancel_remaining cancels them. Each resolved task gets its own history
entry and event, exactly as if it had been updated individually.
PATCH /api/v1/projects/:id/color
Section titled “PATCH /api/v1/projects/:id/color”Update the project’s colour identifier.
Organisation admins and owners can always call this. Other members can update internal projects, projects they are a team member of, and client projects for clients they can access.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Project ID. |
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| color | string | Yes | Colour key. See the field reference above. |
Example request
curl -X PATCH https://api.runnit.io/api/v1/projects/<project-id>/color \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "color": "guava" }'Response
{ "success": true, "project": { "id": "1f0e9a52-...", "color": "guava" } }Errors
400ifcoloris not one of the valid colour keys.404if the project does not exist.403if you are not a member of the project’s organisation or cannot access the project’s client.
PATCH /api/v1/projects/:id/budget
Section titled “PATCH /api/v1/projects/:id/budget”Update the project’s budget amount, currency, or both.
Requires the project owner, a project admin, or an organisation admin or owner.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Project ID. |
Request body (at least one field required)
| Name | Type | Required | Description |
|---|---|---|---|
| budget | number or null | No | Budget amount, zero or greater. Null clears the budget. |
| budgetCurrency | string | No | Exactly 3 letters. Stored uppercase. |
Example request
curl -X PATCH https://api.runnit.io/api/v1/projects/<project-id>/budget \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "budget": 90000, "budgetCurrency": "AUD" }'Response
{ "success": true, "project": { "id": "1f0e9a52-...", "budget": 90000, "budgetCurrency": "AUD" } }Errors
400if neitherbudgetnorbudgetCurrencyis supplied (“No budget updates provided”), the budget is negative, or the currency is not 3 letters.404if the project does not exist.403if you are not the project owner, a project admin, or an organisation admin or owner.
GET /api/v1/projects/:id/milestones
Section titled “GET /api/v1/projects/:id/milestones”List the project’s milestones (key dates).
Any member of the project’s organisation can call this.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Project ID. |
Example request
curl https://api.runnit.io/api/v1/projects/<project-id>/milestones \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true, "milestones": [ { "id": "0a4f6c33-...", "projectId": "1f0e9a52-...", "name": "Client sign-off", "type": "custom", "targetDate": "2026-08-15T00:00:00.000Z", "isFirm": true, "description": "Final approval from the client team.", "sourceKey": null, "orderIndex": 0, "achievedAt": null, "achievedBy": null, "metadata": {}, "createdAt": "2026-07-01T04:12:00.000Z", "updatedAt": "2026-07-01T04:12:00.000Z" } ]}description and sourceKey can be null or absent. achievedAt is the
timestamp when the milestone was marked achieved and achievedBy is the user
who marked it; both are null while the milestone is open. See
Mark a milestone achieved.
Errors
404if the project does not exist.403if you are not a member of the project’s organisation.
POST /api/v1/projects/:id/milestones
Section titled “POST /api/v1/projects/:id/milestones”Create a milestone on the project.
Requires the project owner, a project admin, or an organisation admin or owner.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Project ID. |
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Milestone name. Must not be empty. |
| targetDate | string | Yes | Date in YYYY-MM-DD format. |
| type | string | No | Milestone type label. Defaults to custom. |
| isFirm | boolean | No | Whether the date is firm. Defaults to true. |
| description | string | No | Optional detail. |
| sourceKey | string or null | No | Optional key linking the milestone to its source. |
| orderIndex | integer | No | Sort position, zero or greater. Defaults to 0. |
| metadata | object | No | Free-form metadata. |
Example request
curl -X POST https://api.runnit.io/api/v1/projects/<project-id>/milestones \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "name": "Client sign-off", "targetDate": "2026-08-15", "isFirm": true }'Response
Returns 201 with the created milestone:
{ "success": true, "milestone": { "id": "0a4f6c33-...", "name": "Client sign-off", "targetDate": "2026-08-15T00:00:00.000Z" } }Errors
400ifnameis missing,targetDateis missing or not inYYYY-MM-DDformat, or the date is invalid.404if the project does not exist.403if you are not the project owner, a project admin, or an organisation admin or owner.
PATCH /api/v1/projects/:id/milestones/:milestoneId
Section titled “PATCH /api/v1/projects/:id/milestones/:milestoneId”Update a milestone. Send only the fields you want to change.
Requires the project owner, a project admin, or an organisation admin or owner.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Project ID. |
| milestoneId | UUID | Yes | Milestone ID. Must belong to the project. |
Request body (all optional)
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | No | New name. Must not be empty. |
| targetDate | string | No | Date in YYYY-MM-DD format. |
| type | string | No | Milestone type label. |
| isFirm | boolean | No | Whether the date is firm. |
| description | string or null | No | Detail text. Null or empty clears it. |
| sourceKey | string or null | No | Source key. Null clears it. |
| orderIndex | integer | No | Sort position, zero or greater. |
| metadata | object | No | Replaces the metadata object. |
Example request
curl -X PATCH https://api.runnit.io/api/v1/projects/<project-id>/milestones/<milestone-id> \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "targetDate": "2026-08-22" }'Response
{ "success": true, "milestone": { "id": "0a4f6c33-...", "targetDate": "2026-08-22T00:00:00.000Z" } }Errors
400iftargetDateis not a validYYYY-MM-DDdate.404if the project or milestone does not exist, or the milestone belongs to a different project.403if you are not the project owner, a project admin, or an organisation admin or owner.
DELETE /api/v1/projects/:id/milestones/:milestoneId
Section titled “DELETE /api/v1/projects/:id/milestones/:milestoneId”Delete a milestone from the project.
Requires the project owner, a project admin, or an organisation admin or owner.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Project ID. |
| milestoneId | UUID | Yes | Milestone ID. Must belong to the project. |
Example request
curl -X DELETE https://api.runnit.io/api/v1/projects/<project-id>/milestones/<milestone-id> \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true }Errors
404if the project or milestone does not exist, or the milestone belongs to a different project.403if you are not the project owner, a project admin, or an organisation admin or owner.
POST /api/v1/projects/:id/milestones/:milestoneId/achieve
Section titled “POST /api/v1/projects/:id/milestones/:milestoneId/achieve”Mark a milestone as achieved. This records who marked it and when, and stops any further “milestone due” or “milestone overdue” notifications for it.
The call is idempotent: marking an already achieved milestone keeps the original timestamp and sends no duplicate notifications.
Requires the project owner, a project admin, or an organisation admin or owner.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Project ID. |
| milestoneId | UUID | Yes | Milestone ID. Must belong to the project. |
Example request
curl -X POST https://api.runnit.io/api/v1/projects/<project-id>/milestones/<milestone-id>/achieve \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true, "milestone": { "id": "0a4f6c33-...", "achievedAt": "2026-08-14T02:10:00.000Z", "achievedBy": "9b1d2e40-..." } }Errors
404if the project or milestone does not exist, or the milestone belongs to a different project.403if you are not the project owner, a project admin, or an organisation admin or owner.
DELETE /api/v1/projects/:id/milestones/:milestoneId/achieve
Section titled “DELETE /api/v1/projects/:id/milestones/:milestoneId/achieve”Reopen an achieved milestone. This clears achievedAt and achievedBy, and
the milestone becomes eligible for due and overdue notifications again.
Requires the project owner, a project admin, or an organisation admin or owner.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Project ID. |
| milestoneId | UUID | Yes | Milestone ID. Must belong to the project. |
Example request
curl -X DELETE https://api.runnit.io/api/v1/projects/<project-id>/milestones/<milestone-id>/achieve \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true, "milestone": { "id": "0a4f6c33-...", "achievedAt": null, "achievedBy": null } }Errors
404if the project or milestone does not exist, or the milestone belongs to a different project.403if you are not the project owner, a project admin, or an organisation admin or owner.
POST /api/v1/projects/:id/tasks
Section titled “POST /api/v1/projects/:id/tasks”Create a task in the project. Only the name is required. Without a planned
start, the task is created as pending backlog work with null planned dates
and no schedule segments. Supplying a planned start creates status scheduled
and builds the working-day schedule from the start and estimate.
Runnit validates initial dependencies before it saves the task. If dependency validation, schedule creation, or the dependency cascade fails, the request returns an error and the new task is not retained. You can retry the request without creating a duplicate task.
Requires task:create, or a project owner or project admin override. Supplying
assignedToId also requires task:assign unless the override applies. Managers
have both capabilities by default.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Project ID. |
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Task name. Must not be empty. |
| description | string | No | Task description. |
| priority | string | No | low, medium, high, or critical. Defaults to medium. |
| assignedToId | UUID or null | No | User to assign. Must be an active organisation member. On a client project they must be on the client staffing list or the explicit project team. |
| plannedStartDate | ISO 8601 date | No | Schedule start. Omit it to keep the task unscheduled. |
| estimatedHours | number | No | Greater than 0 and less than 1000. Without a start, the estimate is stored without creating a schedule. With a start, it defaults to the organisation’s dailyCapacityHours value, or 8 when that setting is missing or invalid. |
| dependsOn | UUID[] | No | Initial finish-to-start dependencies from the same project. Requires task-wide update and schedule-management access unless a project override applies. |
Example request
Capture a name-only backlog task:
curl -X POST https://api.runnit.io/api/v1/projects/<project-id>/tasks \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "name": "Confirm meeting follow-up" }'This returns a pending task with null assignment, estimate, and planned dates,
plus empty scheduleSegments and scheduleConflicts arrays.
Create and schedule a planned task:
curl -X POST https://api.runnit.io/api/v1/projects/<project-id>/tasks \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "name": "Draft homepage copy", "description": "First pass for internal review.", "priority": "high", "assignedToId": "<user-uuid>", "plannedStartDate": "2026-07-13", "estimatedHours": 6 }'Response
Returns 201 with the created task, its schedule segments, and any clashes
with the assignee’s existing schedule. Unscheduled tasks return empty arrays:
{ "success": true, "task": { "id": "4be2d871-...", "name": "Draft homepage copy", "status": "scheduled", "priority": "high", "assignedToId": "d3a6f0c1-...", "estimatedHours": 6, "plannedStartDate": "2026-07-13T00:00:00.000Z", "plannedEndDate": "2026-07-13T23:59:59.000Z" }, "scheduleSegments": [ { "startTime": "2026-07-13T09:00:00.000Z", "endTime": "2026-07-13T15:30:00.000Z", "hours": 6, "isOverdue": false, "daysOverdue": 0 } ], "scheduleConflicts": [ { "taskId": "77c0d1aa-...", "startTime": "2026-07-13T09:00:00.000Z", "endTime": "2026-07-13T12:00:00.000Z", "durationMinutes": 180 } ]}scheduleConflicts is empty when the task has no assignee or the assignee is
free. Conflicts are informational only; the task is still created.
Errors
400ifnameis missing,estimatedHoursis out of range, orplannedStartDateis invalid.400if the assignee is not an active member of the organisation, or is neither on the client staffing list nor the explicit project team.404if the project or the assigned user does not exist.403if you are not the project owner, a project admin, or an organisation manager, admin, or owner.
PATCH /api/v1/projects/:id/tasks/:taskId/dates
Section titled “PATCH /api/v1/projects/:id/tasks/:taskId/dates”Move a task to a new start date and rebuild its schedule. The end date is
recalculated from the task’s estimated hours across working days. Scheduling a
pending backlog task this way also sets its status to scheduled, matching a
task created with a planned start. Work that is already in progress, completed,
or cancelled keeps its status.
Requires the project owner, a project admin, or an organisation manager, admin, or owner.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Project ID. |
| taskId | UUID | Yes | Task ID. |
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| plannedStartDate | ISO 8601 date | Yes | New schedule start. |
| plannedEndDate | ISO 8601 date | No | Accepted but not used; the end date is always recalculated from the estimate. |
| allowWeekends | boolean | No | Allow scheduling on weekends. An explicit value updates the task’s saved weekend policy. When omitted, the current policy is preserved; tasks without a saved policy default to weekdays only. |
If the task has no usable estimate, Runnit derives one from its existing
schedule entries, then from its current planned date span, and finally uses the
organisation’s dailyCapacityHours, with 8 hours as the fallback.
The response schedule is also stored as the task’s canonical daily allocation. Later estimate changes and dependency cascades preserve the saved weekend policy. Organisation closure dates are never scheduled.
Example request
curl -X PATCH https://api.runnit.io/api/v1/projects/<project-id>/tasks/<task-id>/dates \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "plannedStartDate": "2026-07-20", "allowWeekends": false }'Response
{ "success": true, "task": { "id": "4be2d871-...", "plannedStartDate": "2026-07-20T00:00:00.000Z", "plannedEndDate": "2026-07-21T00:00:00.000Z" }, "scheduleSegments": [ { "startTime": "2026-07-20T09:00:00.000Z", "endTime": "2026-07-20T17:00:00.000Z", "hours": 8, "isOverdue": false, "daysOverdue": 0 } ]}Errors
400ifplannedStartDateis missing or invalid.400if the schedule cannot be rebuilt for the requested dates.404if the project or task does not exist.403if you are not the project owner, a project admin, or an organisation manager, admin, or owner.
PATCH /api/v1/projects/:id/tasks/:taskId/estimate
Section titled “PATCH /api/v1/projects/:id/tasks/:taskId/estimate”Change a task’s estimated hours. An undated task stores the estimate but remains unscheduled. A dated task with a day-by-day breakdown (manual schedule mode) keeps its planned window when the new total fits that window at the organisation’s daily capacity: each day’s hours are rescaled in proportion so they sum to the new estimate. When the new total does not fit, or the task has an automatic schedule, the schedule rebuilds from the task’s current start and the planned end is recalculated.
Requires task:update:any plus schedule:manage, or a project owner or project
admin override. Managers have both capabilities by default.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Project ID. |
| taskId | UUID | Yes | Task ID. |
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| estimatedHours | number | Yes | Greater than 0 and less than 1000. |
| scheduleMode | string | No | Only auto is accepted. Discards any day-by-day breakdown and rebuilds the schedule automatically, even when the new total fits the pinned window. |
The schedule keeps the task’s existing start date. If the task has no planned
start and no existing schedule entry, the response has null planned dates and
an empty scheduleSegments array. Use the dates endpoint when you are ready to
place it on the calendar.
Example request
curl -X PATCH https://api.runnit.io/api/v1/projects/<project-id>/tasks/<task-id>/estimate \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "estimatedHours": 12 }'Response
{ "success": true, "task": { "id": "4be2d871-...", "estimatedHours": 12 }, "scheduleSegments": [ { "startTime": "2026-07-13T09:00:00.000Z", "endTime": "2026-07-13T17:00:00.000Z", "hours": 8, "isOverdue": false, "daysOverdue": 0 }, { "startTime": "2026-07-14T09:00:00.000Z", "endTime": "2026-07-14T13:00:00.000Z", "hours": 4, "isOverdue": false, "daysOverdue": 0 } ]}Errors
400ifestimatedHoursis missing or out of range.400if the schedule cannot be rebuilt.404if the project or task does not exist.403if you do not have both task-wide update and schedule-management access, and no project owner/admin override applies.
PATCH /api/v1/projects/:id/tasks/:taskId/allocations
Section titled “PATCH /api/v1/projects/:id/tasks/:taskId/allocations”Set an explicit per-day hour breakdown for a task (manual schedule mode). This replaces the task’s automatic schedule with the exact days and hours you supply.
Requires task:update:any plus schedule:manage, or a project owner or project
admin override. Managers have both capabilities by default.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Project ID. |
| taskId | UUID | Yes | Task ID. Must belong to the project. |
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| allocations | array | Yes | At least one entry. |
| allocations[].date | ISO 8601 date | Yes | The day to allocate hours to. |
| allocations[].hours | number | Yes | Greater than 0 and less than 24. |
Example request
curl -X PATCH https://api.runnit.io/api/v1/projects/<project-id>/tasks/<task-id>/allocations \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "allocations": [ { "date": "2026-07-13", "hours": 4 }, { "date": "2026-07-15", "hours": 3.5 } ] }'Response
{ "success": true, "task": { "id": "4be2d871-...", "estimatedHours": 7.5 }, "scheduleSegments": [ { "startTime": "2026-07-13T09:00:00.000Z", "endTime": "2026-07-13T13:00:00.000Z", "hours": 4, "isOverdue": false, "daysOverdue": 0 }, { "startTime": "2026-07-15T09:00:00.000Z", "endTime": "2026-07-15T12:30:00.000Z", "hours": 3.5, "isOverdue": false, "daysOverdue": 0 } ]}Errors
400ifallocationsis empty, a date is invalid, or hours are out of range.400if the manual schedule cannot be applied.404if the project or task does not exist, or the task belongs to a different project.403if you do not have both task-wide update and schedule-management access, and no project owner/admin override applies.
PATCH /api/v1/projects/:id/tasks/:taskId/assignee
Section titled “PATCH /api/v1/projects/:id/tasks/:taskId/assignee”Assign a task to a user, or unassign it. The task’s schedule entries move to the new assignee.
Requires the project owner, a project admin, or an organisation manager, admin, or owner.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Project ID. |
| taskId | UUID | Yes | Task ID. Must belong to the project. |
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| assignedToId | UUID or null | No | User to assign. Null (or omitted) removes the assignment. Must be an active organisation member. On a client project they must be on the client staffing list or the explicit project team. |
Example request
curl -X PATCH https://api.runnit.io/api/v1/projects/<project-id>/tasks/<task-id>/assignee \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "assignedToId": "<user-uuid>" }'Response
{ "success": true, "task": { "id": "4be2d871-...", "name": "Draft homepage copy", "assignedToId": "d3a6f0c1-...", "assignee": { "id": "d3a6f0c1-...", "name": "Alex Chen", "email": "alex@example.com" } }, "scheduleConflicts": []}scheduleConflicts lists the new assignee’s overlapping schedule entries in
the task’s window, in the same shape as the task creation endpoint. It is
informational only; the assignment still happens.
Errors
400ifassignedToIdis not a UUID.400if the assignee is not an active member of the organisation, or is neither on the client staffing list nor the explicit project team.404if the project, task, or assigned user does not exist.403if you are not the project owner, a project admin, or an organisation manager, admin, or owner.
GET /api/v1/projects/:id/assignee-options
Section titled “GET /api/v1/projects/:id/assignee-options”List all active organisation staff and their staffing eligibility for this project. This single response supports both task assignment and the project team selector. On a client project, the response distinguishes the client staffing list from an explicit project-only override.
Requires the project owner, a project admin, or an organisation manager, admin, or owner.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Project ID. |
Example request
curl https://api.runnit.io/api/v1/projects/<project-id>/assignee-options \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true, "hasClientRestrictions": true, "options": [ { "id": "d3a6f0c1-...", "name": "Alex Chen", "email": "alex@example.com", "avatarUrl": "https://...", "roleTitle": "Creative Director", "inProjectTeam": true, "clientEligible": false, "eligibleForAssignment": true, "staffingSource": "project_member" } ]}avatarUrl and roleTitle can be absent. Use eligibleForAssignment to build
an assignee control. A false value means the person must first be added to the
project team. staffingSource is one of client_team, open_client,
project_member, or organization.
Errors
404if the project does not exist.403if you are not the project owner, a project admin, or an organisation manager, admin, or owner.
Custom table columns
Section titled “Custom table columns”Projects can carry custom columns for the task table view: text, long text, number, dropdown, date, checkbox, link, and rating. A column is defined at one of three scopes:
project: exists on this project only;client: appears on every project for one client organisation; andorganization: appears on every project in the organisation.
A project resolves its effective columns with project definitions taking
precedence over client definitions, which take precedence over organisation
definitions, matched by key. A project-scope column with the same key as an
inherited one overrides it on that project only, and an override keeps the
inherited key and type so values stay comparable across projects.
Column values live on each task in metadata.tableValues, keyed by the
column’s key.
GET /api/v1/projects/:id/table-columns
Section titled “GET /api/v1/projects/:id/table-columns”List the effective custom columns for a project.
Any member of the project’s organisation can call this. Pass
includeArchived=true to also receive archived and hidden columns; this only
takes effect when you can manage columns on the project.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Project ID. |
Example request
curl https://api.runnit.io/api/v1/projects/<project-id>/table-columns \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true, "columns": [ { "id": "7c2d91aa-...", "projectId": null, "organizationId": "9b1f2e04-...", "clientOrganizationId": null, "scope": "organization", "key": "reviewStage", "title": "Review stage", "type": "dropdown", "settings": { "options": [ { "key": "onTrack", "label": "On Track", "color": "matcha" }, { "key": "atRisk", "label": "At Risk", "color": "mandarin" } ] }, "orderIndex": 0, "isArchived": false, "isOverride": false, "isHidden": false, "createdAt": "2026-07-09T10:00:00.000Z", "updatedAt": "2026-07-09T10:00:00.000Z" } ], "canManage": true, "canManageOrg": true, "defaultColumns": ["status", "assignee", "hours"], "defaultColumnsSource": "organization"}defaultColumnsis the configured default visible column set for this project’s task table, in display order, ornullwhen nothing is configured.defaultColumnsSourcenames where it came from:boardwhen the project’s kanban board sets its own columns,organizationwhen the organisation setting applies, ornull. These are defaults only: each person’s own saved layout and saved views still take precedence in the app.scopetells you where the definition lives.isOverrideis true when a project-scope column shadows an inherited one, andoverridesScopethen names the shadowed scope.canManagereports whether you can manage project-scope columns and task values.canManageOrgreports whether you hold the shared table columns permission, which controls client- and organisation-scope definitions.settingsvaries by type: dropdowns carryoptions(each withkey,label, and acolorfromguava,acai,matcha,coffee,mandarin,neutral), numbers can carryunitandprecision, and ratings carrymax.- Any type except
linkcan also carrysettings.ai:{ "enabled": true, "prompt": "...", "sourceColumns": ["otherKey"], "autoFill": true, "includeFinancials": true }. When enabled, the fill endpoint below writes AI-generated values of the column’s type, validated exactly like manual input. Enabled AI columns always refresh automatically, soautoFillis retained for compatibility and is always returned astrue. WithincludeFinancials, each task’s computed financial snapshot (assignee bill rate, planned cost, actual logged hours/cost, cost variance) is added to the model context; running such fills requires the financial reports permission, and the generated values are visible to everyone who can see the table.
Errors
404if the project does not exist.403if you are not a member of the project’s organisation.
POST /api/v1/projects/:id/table-columns
Section titled “POST /api/v1/projects/:id/table-columns”Create a custom column.
Project-scope columns require the project owner, a project admin, or an organisation manager, admin, or owner. Client- and organisation-scope columns require the shared table columns permission, which organisation admins and owners hold by default and can grant to other roles or people.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Project ID. |
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| title | string | Yes | Column name, up to 255 characters. The stable key is generated from it. |
| type | string | Yes | One of text, long_text, number, dropdown, date, checkbox, link, rating. |
| scope | string | No | One of project (default), client, organization. client requires the project to have a client organisation. |
| settings | object | No | Type-specific settings. Dropdowns require at least one option. |
Example request
curl -X POST https://api.runnit.io/api/v1/projects/<project-id>/table-columns \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "title": "Review stage", "type": "dropdown", "scope": "organization", "settings": { "options": [ { "label": "On Track", "color": "matcha" }, { "label": "At Risk", "color": "mandarin" } ] } }'Response
Returns 201 with the created column in the same shape as the list endpoint.
When the column has AI enabled, Runnit starts filling its existing tasks in
the background and the response returns immediately. Track the run with the
fill status endpoint below. A provider failure does not undo the column
creation; a later automatic or manual refresh can retry. The response’s
aiRefresh field is deprecated and always null.
Errors
400if the title is empty, the type is unsupported, the settings are invalid for the type, the scope limit of 50 columns is reached, orclientscope is requested on a project without a client organisation.404if the project does not exist.403if you lack manage rights for the requested scope (shared scopes need the shared table columns permission).
POST /api/v1/projects/:id/table-columns/overrides
Section titled “POST /api/v1/projects/:id/table-columns/overrides”Create a project-scope shadow of an inherited column: an override you can rename and reconfigure on this project, or a per-project hide.
Requires the project owner, a project admin, or an organisation manager, admin, or owner.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| key | string | Yes | Key of the inherited client- or organisation-scope column. |
| hidden | boolean | No | When true, the column is hidden on this project instead of overridden. Defaults to false. |
Example request
curl -X POST https://api.runnit.io/api/v1/projects/<project-id>/table-columns/overrides \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "key": "reviewStage", "hidden": false }'Response
Returns 201 with the created project-scope column. Delete this column to
restore the inherited definition.
Errors
400if no inherited column has that key, or the project already overrides it.404if the project does not exist.403if you cannot manage columns on this project.
PATCH /api/v1/projects/:id/table-columns/:columnId
Section titled “PATCH /api/v1/projects/:id/table-columns/:columnId”Update a column’s title, settings, order, or archive state. Send only the fields you want to change. The column’s type and key never change.
Project-scope columns require project manage rights. Client- and organisation-scope columns require the shared table columns permission, and changes apply to every project that inherits the column.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| title | string | No | New column name. |
| settings | object | No | Replacement settings, validated for the column’s type. |
| orderIndex | integer | No | Sort position, zero or greater. |
| isArchived | boolean | No | Archive or restore the column. Archived columns keep their values but stop appearing. |
Response
Returns the updated column. When the update changes an AI column’s settings
and the column is not archived, Runnit refreshes its cells in the background
and the response returns immediately. Track the run with the fill status
endpoint below. The response’s aiRefresh field is deprecated and always
null.
Errors
400if the settings are invalid for the column’s type.404if the column does not exist or is not visible from this project.403if you lack manage rights for the column’s scope.
DELETE /api/v1/projects/:id/table-columns/:columnId
Section titled “DELETE /api/v1/projects/:id/table-columns/:columnId”Delete a column definition. Task values stored under the column’s key remain in task metadata but are no longer shown. Deleting a project-scope override or hide restores the inherited definition.
The same scope-based permissions as the update endpoint apply.
Errors
404if the column does not exist or is not visible from this project.403if you lack manage rights for the column’s scope.
POST /api/v1/projects/:id/table-columns/refresh
Section titled “POST /api/v1/projects/:id/table-columns/refresh”Refresh stale cells across every visible AI-enabled column in a project. A cell
is stale when it has no generated result, its task or prompt inputs changed, or
its generation time is older than staleBefore. Cells that are still current
are skipped before an AI request is made.
Runnit’s task table calls this automatically on load with the start of the user’s current local day. You can also call it for an integration that updates or displays project tasks. Any member who can view the project can request the refresh. The automatic run uses each column’s owner and existing permissions for its AI work.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| taskIds | UUID[] | No | Restrict the refresh to up to 100 tasks in this project. Omit it to check every task. |
| staleBefore | ISO 8601 timestamp | No | Refresh values generated before this time, even when their inputs have not changed. |
| async | boolean | No | When true, start the refresh in the background and return 202 with { "success": true, "started": true } immediately. Track progress with the fill status endpoint. Defaults to false, which waits and returns the summary below. |
Example request
curl -X POST https://api.runnit.io/api/v1/projects/<project-id>/table-columns/refresh \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "staleBefore": "2026-07-17T00:00:00.000+10:00" }'Response
{ "success": true, "summary": { "filled": 4, "skipped": 11, "failed": 0, "columns": [ { "columnKey": "deliveryRisk", "filled": 4, "skipped": 11, "failed": 0, "results": [ { "taskId": "8174ae2c-...", "status": "filled", "value": "high" } ] } ] }}Large projects are checked in groups of 100 tasks. Matching refreshes already in progress are reused, and rapid task edits are grouped before automatic refresh, which limits duplicate AI calls.
Errors
400iftaskIdscontains more than 100 entries, or ifstaleBeforeis not a valid timestamp.404if the project does not exist or a requested task is outside it.403if you cannot view the project.503if an AI column needs a refresh but no provider is configured.
GET /api/v1/projects/:id/table-columns/fill-status
Section titled “GET /api/v1/projects/:id/table-columns/fill-status”Live progress of the project’s background AI fill runs: the runs started by
creating or updating an AI column, by an async refresh, or by a manual fill.
Any member who can view the project can read it. Finished runs stay in the
feed for about a minute so a final poll can read the outcome.
Example request
curl https://api.runnit.io/api/v1/projects/<project-id>/table-columns/fill-status \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true, "runs": [ { "columnKey": "deliveryRisk", "total": 88, "done": 30, "filled": 28, "skipped": 0, "failed": 2, "startedAt": "2026-07-31T00:12:35.429Z", "finishedAt": null } ]}runs is empty when nothing is running and nothing finished recently.
Progress is tracked in memory, so a server restart clears the feed; the cells
themselves are recovered by the next automatic refresh.
Errors
404if the project does not exist.403if you cannot view the project.
POST /api/v1/projects/:id/table-columns/preview
Section titled “POST /api/v1/projects/:id/table-columns/preview”Try a column draft’s AI instruction on a small sample of the project’s tasks without saving anything. No cell values, provenance, or project history are written. The task table’s column editor uses this for its Test on sample tasks action, and you can call it while designing a column through the API.
The draft is validated exactly like the create endpoint (dropdowns need at least one option, AI needs a prompt, link columns cannot use AI). Requires the same manage rights as the fill endpoint. Previewing a draft that reads financial data requires the financial reports permission.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| column | object | Yes | The draft: title, type, and optional settings in the same shape as the create endpoint, including the ai block. |
| columnId | UUID | No | When previewing changes to an existing column, its id, so source-column context resolves against the saved key. |
| sampleSize | integer | No | Number of sample tasks, 1 to 5. Defaults to 3. Samples are spread evenly across the task list. |
Example request
curl -X POST https://api.runnit.io/api/v1/projects/<project-id>/table-columns/preview \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "column": { "title": "Delivery risk", "type": "dropdown", "settings": { "options": [ { "label": "High", "color": "mandarin" }, { "label": "Low", "color": "matcha" } ], "ai": { "enabled": true, "prompt": "Classify this task'\''s delivery risk." } } }, "sampleSize": 2 }'Response
{ "success": true, "items": [ { "taskId": "8174ae2c-...", "taskName": "Stakeholder alignment and governance", "status": "ok", "value": "low", "valueLabel": "Low", "model": "gpt-5.6-luna" } ]}Dropdown values are option keys; valueLabel carries the matching option
label. A status of failed includes a safe error message, for example
when the model’s value fails the column’s validation.
Errors
400if the draft is invalid or AI is not enabled on it.404if the project orcolumnIddoes not exist.403if you cannot manage columns, or the draft reads financial data without the financial reports permission.503if no AI provider is configured for the environment.
POST /api/v1/projects/:id/table-columns/:columnId/fill
Section titled “POST /api/v1/projects/:id/table-columns/:columnId/fill”Run the AI fill for a column whose settings include an enabled ai block.
The model reads each task’s built-in fields (name, description, status,
priority, tags, dates) plus any configured source columns, and writes a value
of the column’s type. Every value passes the same validation as manual input,
and provenance (model, timestamp) is stored per cell.
Requires the project owner, a project admin, or an organisation manager, admin, or owner.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| taskIds | UUID[] | No | Restrict the fill to specific tasks. Defaults to every task in the project (up to 100). |
| onlyEmpty | boolean | No | Skip tasks that already have a value for this column. Defaults to false. |
| onlyStale | boolean | No | Skip cells whose task inputs and instruction are unchanged since they were generated. Defaults to false. |
Example request
curl -X POST https://api.runnit.io/api/v1/projects/<project-id>/table-columns/<column-id>/fill \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "onlyEmpty": true }'Response
{ "success": true, "summary": { "columnKey": "deliveryRisk", "filled": 15, "skipped": 2, "failed": 0, "results": [ { "taskId": "8174ae2c-...", "status": "filled", "value": "high" } ] }}Failed cells report status: "failed" with an error message and leave the
existing value untouched.
Errors
400if the column has no AI configuration, a task id is not in this project, or more than 100 tasks are requested.404if the column does not exist or is hidden on this project.403if you cannot edit task fields on this project.503if no AI provider is configured for the environment.
PATCH /api/v1/projects/:id/tasks/:taskId/table-values
Section titled “PATCH /api/v1/projects/:id/tasks/:taskId/table-values”Set custom-column values on a task. Values are validated against the
project’s effective columns and merged into the task’s
metadata.tableValues; send null to clear a value.
Requires the project owner, a project admin, or an organisation manager, admin, or owner.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| values | object | Yes | Keyed by column key. Text, long text, date (YYYY-MM-DD), and dropdown option keys are strings; number and rating are numbers; checkbox is a boolean; link is { "url": "https://...", "label": "optional" }. |
Example request
curl -X PATCH https://api.runnit.io/api/v1/projects/<project-id>/tasks/<task-id>/table-values \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "values": { "reviewStage": "atRisk", "poNumber": "PO-1044", "approved": true } }'Response
Returns the updated task. The stored values are in task.metadata.tableValues.
Errors
400if a key does not match any effective column, a value fails the column’s validation, or the column is hidden on this project.404if the project or task does not exist.403if you cannot edit task fields on this project.
GET /api/v1/projects/:id/task-costs
Section titled “GET /api/v1/projects/:id/task-costs”Per-task rollup of logged time costs, priced with the rate snapshots captured
on each time entry (the same maths as the cost tools): actualCost uses the
actual cost rate, billableCost uses the billable rate and only counts
billable entries.
Requires the financial reports permission (organisation managers and above by
default). Unlike most read endpoints this is hard-enforced: callers without
the permission receive 403 regardless of the organisation’s permission
enforcement mode.
Response
{ "success": true, "taskCosts": [ { "taskId": "8174ae2c-...", "hours": 3, "billableHours": 2, "actualCost": 360, "billableCost": 400, "currency": "AUD" } ], "assigneeRates": [ { "userId": "213ae491-...", "rate": 290, "currency": "AUD" } ]}Tasks with no logged time are omitted from taskCosts. assigneeRates
carries the resolved bill rate for every task assignee on the project,
including contributors who are not formal project members (resolution:
project member rate snapshot, then project member rate, then organisation
membership rate, then user rate). The app uses this endpoint for the
view-only financial columns on the project task table: Rate, Actual hours,
Planned billable, Actual billable, and Billable variance (actual minus
planned). Those columns only exist for users holding the same permission.
Task dependencies
Section titled “Task dependencies”Tasks support finish-to-start dependencies (dependsOn, an array of task ids
in the same project). Dependencies are validated on write: self-references,
tasks from other projects, cycles (including transitive ones), and lists over
25 entries are rejected with 400.
Dependencies are self-maintaining. Whenever a task’s dates change (date
edits, allocation edits, hour changes, or a dependency edit), every dependent
that would now start before its latest predecessor ends is pushed forward
through the real scheduler: hours re-spread across working days, weekends and
organisation closure dates skipped, manual day-by-day hour patterns
preserved, completed and cancelled tasks never moved, and tasks with slack
left alone (the cascade is push-only). The affected endpoints return a
cascade object:
{ "cascade": { "shifted": [ { "taskId": "...", "taskName": "Design", "fromStart": "2026-07-15T00:00:00.000Z", "toStart": "2026-07-23", "fromEnd": "2026-07-17T00:00:00.000Z", "toEnd": "2026-07-24" } ], "skipped": [], "evaluated": 2 }}skipped lists dependents that could not be moved automatically (reason
unscheduled or rebuild_failed) with the earliest start they should have.
When a task completes and it was the last thing blocking a dependent, the
dependent’s assignee receives a task unblocked notification.
PUT /api/v1/projects/:id/tasks/:taskId/dependencies
Section titled “PUT /api/v1/projects/:id/tasks/:taskId/dependencies”Replace a task’s dependency list. Requires the project owner, a project admin, or an organisation manager, admin, or owner.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| dependsOn | UUID[] | Yes | Predecessor task ids (same project, max 25, acyclic). Empty array clears. |
Response
Returns the updated task and the cascade summary described above.
POST /api/v1/projects/:id/tasks also accepts dependsOn, applying the same
validation and shifting the new task after its predecessors when needed.
Invalid dependencies are rejected before the task is saved.
Saved table views
Section titled “Saved table views”The task table’s layout (visible columns, order, widths, grouping, sorts, and
filters) can be saved as a named view. Personal views belong to one user;
shared views (created with "shared": true) are visible to everyone on the
project. One personal and one shared view can be flagged as the default; the
personal default wins when both exist.
GET /api/v1/projects/:id/views
Section titled “GET /api/v1/projects/:id/views”List the views visible to you: your personal views plus the project’s shared views. Any member of the project’s organisation can call this.
Response
{ "success": true, "views": [ { "id": "4c1d22aa-...", "projectId": "1f0e9a52-...", "userId": null, "name": "Team standard", "isDefault": true, "config": { "groupBy": "priority", "sorts": [], "columns": [], "filters": {} }, "createdAt": "2026-07-10T00:00:00.000Z", "updatedAt": "2026-07-10T00:00:00.000Z" } ], "canManageShared": true}userId is null for shared views. config is the stored layout object; the
app treats it as opaque.
POST /api/v1/projects/:id/views
Section titled “POST /api/v1/projects/:id/views”Create a saved view.
Personal views can be created by any member with project access. Shared views
("shared": true) require the project owner, a project admin, or an
organisation manager, admin, or owner.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | View name, up to 120 characters. |
| config | object | Yes | The layout to store (up to 20 KB). |
| shared | boolean | No | Create as a shared project view. Defaults to false. |
| isDefault | boolean | No | Flag as the default in its scope. Clears any previous default. |
Response
Returns 201 with the created view.
Errors
400if the config is not an object, is too large, or the 30-views-per-scope limit is reached.403ifsharedis requested without manage rights.404if the project does not exist.
PATCH /api/v1/projects/:id/views/:viewId
Section titled “PATCH /api/v1/projects/:id/views/:viewId”Update a view’s name, config, or default flag. You can modify your own views; shared views also require project manage rights.
Errors
403if the view is not yours (personal views of other users cannot be modified by anyone, including managers).404if the view does not exist on this project.
DELETE /api/v1/projects/:id/views/:viewId
Section titled “DELETE /api/v1/projects/:id/views/:viewId”Delete a view. The same ownership rules as the update endpoint apply.
DELETE /api/v1/projects/:id
Section titled “DELETE /api/v1/projects/:id”Delete a project and everything attached to it.
Requires the project owner, a project admin, or an organisation admin or owner.
Deleting a project removes, in one operation:
- the project itself;
- all of its tasks;
- all schedule entries for those tasks;
- all project team memberships;
- the asset collections linked to the project; and
- every asset in those collections, including stored files and file versions.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Project ID. |
Example request
curl -X DELETE https://api.runnit.io/api/v1/projects/<project-id> \ -H 'Authorization: Bearer rnk_your_key'Response
Returns a summary of what was removed:
{ "success": true, "projectId": "1f0e9a52-...", "tasksSoftDeleted": 14, "scheduleEntriesDeleted": 31, "membersRemoved": 5, "collectionsDeleted": 2, "assetsDeleted": 27}Errors
404if the project does not exist or was already deleted.403if you are not the project owner, a project admin, or an organisation admin or owner.
Next steps
Section titled “Next steps”Work with individual tasks through the Tasks API, or group projects with the Master Projects API.