Skip to content

Scheduling API

The Scheduling API reads the schedule calendar Runnit builds from task assignments: who is booked on what, when, and for how long. It also manages organisation closure dates (public holidays and office closures), per-user blocked-out dates (leave and appointments), both of which the scheduler skips when it books work, and recurring working patterns, which set how long each person’s working days actually are.

Unscheduled project tasks do not appear here. They remain available through project and task APIs until a planned start creates schedule entries. This keeps backlog capture separate from resource capacity.

Planning entries are included. Each schedule row carries projectStatus and bookingState (tentative, held, or confirmed). Use all entries for capacity planning. A personal calendar should show only rows with a projectStatus other than draft and a bookingState of confirmed. This also protects against older confirmed rows on a draft project. Google/Microsoft sync and calendar feeds apply the same rule. For the user’s actionable task list, use GET /api/v1/tasks/my.

Deleted tasks do not appear, even if legacy schedule data still refers to them.

Authentication: every endpoint requires an API key. See API Keys & Authentication.

Get the schedule entries for one or more users. Each entry is one booked segment of a task on a user’s calendar, enriched with task, project, and assignee details. Entries for projects you cannot see are filtered out.

Any organisation member with schedule access can call this. Every requested user must share an organisation with you.

Query parameters

NameTypeRequiredDescription
userIdsstringYesComma-separated list of user IDs to fetch schedules for.

The endpoint returns each user’s full schedule; there is no date-range filter. Filter client-side if you only need a window.

Segment start and end values are absolute timestamps. Convert them to the organisation timezone when displaying a calendar day. Shared tasks have a booking for each assigned person; use one person’s segments for a per-person daily plan. This user-filtered endpoint does not return unassigned bookings. The same reads are available through MCP call_public_api with the same authority.

Example request

Terminal window
curl 'https://api.runnit.io/api/v1/schedule?userIds=<user-uuid-1>,<user-uuid-2>' \
-H 'Authorization: Bearer rnk_your_key'

Response

A JSON array of schedule entries:

[
{
"id": "8fd2b3c4-...",
"taskId": "4be2d871-...",
"taskNumber": "TASK-014",
"projectId": "1f0e9a52-...",
"projectName": "Winter Launch",
"projectStatus": "active",
"clientName": "Northwind",
"name": "Draft homepage copy",
"description": "First pass for internal review.",
"status": "scheduled",
"priority": "high",
"assignedToId": "d3a6f0c1-...",
"assignedTo": {
"id": "d3a6f0c1-...",
"firstName": "Alex",
"lastName": "Chen",
"displayName": "Alex Chen",
"avatarUrl": "https://...",
"currentTitle": "Creative Director"
},
"plannedStartDate": "2026-07-13T09:00:00.000Z",
"plannedEndDate": "2026-07-13T15:30:00.000Z",
"durationMinutes": 360,
"allocatedHours": 6,
"estimatedHours": 6,
"segmentIndex": 0,
"totalSegments": 1,
"isOverdue": false,
"daysOverdue": 0,
"bookingState": "confirmed",
"tags": ["copy", "homepage"],
"metadata": {}
}
]

A task that spans several days appears as several entries; segmentIndex and totalSegments tell you which piece you are looking at. plannedStartDate and plannedEndDate are the segment’s start and end times, not the task’s overall dates. status is one of pending, scheduled, in_progress, completed, cancelled; priority is one of low, medium, high, critical. clientName, description, taskNumber, assignedTo, allocatedHours, estimatedHours, and tags can be null or absent.

An empty array means the requested users have no schedule entries. It does not mean their accessible projects have no backlog tasks.

Errors

  • 400 if userIds is missing, or you have no active organisation.
  • 403 if any requested user does not share an organisation with you, or none of the entries belong to projects you can see.

Get the milestones of every project that appears in the selected users’ schedules. Useful for drawing key dates alongside the schedule calendar.

Any organisation member with schedule access can call this. Every requested user must share an organisation with you.

Query parameters

NameTypeRequiredDescription
userIdsstringYesComma-separated list of user IDs.

Example request

Terminal window
curl 'https://api.runnit.io/api/v1/schedule/milestones?userIds=<user-uuid-1>,<user-uuid-2>' \
-H 'Authorization: Bearer rnk_your_key'

Response

A JSON array of milestones:

[
{
"id": "0a4f6c33-...",
"projectId": "1f0e9a52-...",
"projectName": "Winter Launch",
"name": "Client sign-off",
"type": "custom",
"targetDate": "2026-08-15",
"isFirm": true,
"description": "Final approval from the client team.",
"sourceKey": null,
"orderIndex": 0,
"metadata": {}
}
]

targetDate is a plain YYYY-MM-DD date. description and sourceKey can be null. An empty array means the selected users have no scheduled work, or their projects have no milestones.

Errors

  • 400 if userIds is missing, or you have no active organisation.
  • 403 if any requested user does not share an organisation with you.

Get a task with its project, assigned user, and subtasks in one call. This is the detail view behind a schedule entry.

Any organisation member with schedule access can call this, for tasks in projects their organisation can see.

Path parameters

NameTypeRequiredDescription
taskIdUUIDYesTask ID.

Example request

Terminal window
curl https://api.runnit.io/api/v1/schedule/tasks/<task-id> \
-H 'Authorization: Bearer rnk_your_key'

Response

The task object with related records attached:

{
"id": "4be2d871-...",
"projectId": "1f0e9a52-...",
"name": "Draft homepage copy",
"status": "scheduled",
"priority": "high",
"progress": 0,
"assignedToId": "d3a6f0c1-...",
"estimatedHours": 6,
"plannedStartDate": "2026-07-13T00:00:00.000Z",
"plannedEndDate": "2026-07-14T00:00:00.000Z",
"project": { "id": "1f0e9a52-...", "name": "Winter Launch", "status": "active" },
"assignedUser": { "id": "d3a6f0c1-...", "displayName": "Alex Chen", "email": "alex@example.com" },
"subtasks": []
}

assignedUser is null for unassigned tasks. The task and project include their other standard fields.

Errors

  • 404 if the task or its project does not exist.
  • 403 if the project belongs to an organisation you cannot access.

Closure dates are days the organisation does not work: public holidays and office closures. The scheduler skips them when booking task time. All closure-date endpoints operate on your own organisation.

A closure date looks like this:

{
"id": "e19c50f7-...",
"organizationId": "8c2d41b7-...",
"date": "2026-12-25",
"name": "Christmas Day",
"description": "Public holiday",
"type": "holiday",
"isRecurring": true,
"createdAt": "2026-01-05T00:00:00.000Z",
"updatedAt": "2026-01-05T00:00:00.000Z",
"createdById": "d3a6f0c1-..."
}

type is one of holiday, closure, other. description and createdById can be null.

Reading closure dates is open to any organisation member. Creating, updating, and deleting them requires permission to manage organisation settings, which organisation admins and owners have.

List your organisation’s closure dates, optionally filtered to a date range.

Any organisation member can call this.

Query parameters

NameTypeRequiredDescription
startDateISO 8601 dateNoStart of the range. Only applied when endDate is also supplied.
endDateISO 8601 dateNoEnd of the range. Only applied when startDate is also supplied.

Example request

Terminal window
curl 'https://api.runnit.io/api/v1/closure-dates?startDate=2026-12-01&endDate=2027-01-31' \
-H 'Authorization: Bearer rnk_your_key'

Response

A JSON array of closure dates in the shape shown above.

Errors

  • 404 if your account is not part of any organisation.

Check whether a specific date is a closure date for your organisation.

Any organisation member can call this.

Query parameters

NameTypeRequiredDescription
dateISO 8601 dateYesThe date to check.

Example request

Terminal window
curl 'https://api.runnit.io/api/v1/closure-dates/check?date=2026-12-25' \
-H 'Authorization: Bearer rnk_your_key'

Response

{ "isClosureDate": true }

Errors

  • 400 if date is missing or not a valid date.

Get a single closure date by ID.

Any organisation member can call this, for closure dates in their own organisation.

Path parameters

NameTypeRequiredDescription
idUUIDYesClosure date ID.

Example request

Terminal window
curl https://api.runnit.io/api/v1/closure-dates/<closure-date-id> \
-H 'Authorization: Bearer rnk_your_key'

Response

A single closure date object in the shape shown above.

Errors

  • 404 if the closure date does not exist or belongs to a different organisation.

Create a closure date.

Requires permission to manage organisation settings (organisation admins and owners).

Request body

NameTypeRequiredDescription
dateISO 8601 dateYesThe closure date.
namestringYesLabel, for example “Christmas Day”.
descriptionstringNoExtra detail.
typestringNoholiday, closure, or other.
isRecurringbooleanNoWhether the closure repeats every year.

Example request

Terminal window
curl -X POST https://api.runnit.io/api/v1/closure-dates \
-H 'Authorization: Bearer rnk_your_key' \
-H 'Content-Type: application/json' \
-d '{ "date": "2026-12-25", "name": "Christmas Day", "type": "holiday", "isRecurring": true }'

Response

Returns 201 with the created closure date object.

Errors

  • 400 if date or name is missing, or type is invalid.
  • 403 if you cannot manage organisation settings.
  • 404 if your account is not part of any organisation.

Create several closure dates in one request, for example a full public holiday calendar.

Requires permission to manage organisation settings (organisation admins and owners).

Request body

NameTypeRequiredDescription
datesarrayYesAt least one entry.
dates[].dateISO 8601 dateYesThe closure date.
dates[].namestringYesLabel.
dates[].descriptionstringNoExtra detail.
dates[].typestringNoholiday, closure, or other.
dates[].isRecurringbooleanNoWhether the closure repeats every year.

Example request

Terminal window
curl -X POST https://api.runnit.io/api/v1/closure-dates/bulk \
-H 'Authorization: Bearer rnk_your_key' \
-H 'Content-Type: application/json' \
-d '{
"dates": [
{ "date": "2026-12-25", "name": "Christmas Day", "type": "holiday", "isRecurring": true },
{ "date": "2026-12-28", "name": "Boxing Day (observed)", "type": "holiday", "isRecurring": false }
]
}'

Response

Returns 201 with a JSON array of the created closure date objects.

Errors

  • 400 if dates is empty, or any entry is missing date or name.
  • 403 if you cannot manage organisation settings.

Update a closure date. Send only the fields you want to change.

Requires permission to manage organisation settings (organisation admins and owners).

Path parameters

NameTypeRequiredDescription
idUUIDYesClosure date ID.

Request body (all optional)

NameTypeRequiredDescription
dateISO 8601 dateNoNew date.
namestringNoNew label.
descriptionstringNoExtra detail.
typestringNoholiday, closure, or other.
isRecurringbooleanNoWhether the closure repeats every year.

Example request

Terminal window
curl -X PATCH https://api.runnit.io/api/v1/closure-dates/<closure-date-id> \
-H 'Authorization: Bearer rnk_your_key' \
-H 'Content-Type: application/json' \
-d '{ "isRecurring": false }'

Response

The updated closure date object.

Errors

  • 400 if a field fails validation.
  • 403 if you cannot manage organisation settings.
  • 404 if the closure date does not exist or belongs to a different organisation.

Delete a closure date.

Requires permission to manage organisation settings (organisation admins and owners).

Path parameters

NameTypeRequiredDescription
idUUIDYesClosure date ID.

Example request

Terminal window
curl -X DELETE https://api.runnit.io/api/v1/closure-dates/<closure-date-id> \
-H 'Authorization: Bearer rnk_your_key'

Response

Returns 204 with no body.

Errors

  • 403 if you cannot manage organisation settings.
  • 404 if the closure date does not exist or belongs to a different organisation.

Blocked-out dates are days one user is unavailable: leave, appointments, or anything else. A block can cover the whole of each day or only part of it. The scheduler treats whole-day blocks like closure dates scoped to that person and never books work on them; a partially blocked day keeps working capacity equal to that day’s capacity minus the blocked hours. That day’s capacity comes from the person’s working pattern when they have one, and from the organisation’s daily capacity otherwise. Four hours of leave on a four-hour day therefore removes the whole day. These endpoints let an external system, such as an HR platform, sync approved leave into Runnit, including half-day leave sent directly as hours, so capacity planning stays accurate. All blocked-date endpoints operate on your own organisation.

A blocked-date range looks like this:

{
"id": "5b7d20aa-...",
"organizationId": "8c2d41b7-...",
"userId": "d3a6f0c1-...",
"startDate": "2027-09-06",
"endDate": "2027-09-10",
"reason": "Synced from HR",
"type": "leave",
"hoursUnavailable": null,
"createdAt": "2027-08-30T00:00:00.000Z",
"updatedAt": "2027-08-30T00:00:00.000Z",
"createdById": "d3a6f0c1-..."
}

type is one of leave, appointment, other (the default). reason and createdById can be null.

hoursUnavailable is null for a whole-day block. A number makes the block partial: that many hours are unavailable on each day of the range. Multiple partial blocks covering the same day add together, and when the remaining capacity reaches zero the day behaves exactly like a whole-day block. Values at or above that day’s capacity also behave as whole-day blocks. The stored hours are never rewritten, so a later change to the organisation’s daily capacity, or to the person’s working pattern, re-evaluates every partial day automatically.

Permissions. An API key acts as its owning user. Everyone can manage their own blocked dates. Reading another user’s blocked dates requires schedule view access; creating, changing, or removing another user’s requires schedule management access (managers and above by default). When someone else changes a user’s blocked dates, the affected user is notified and the change is recorded in the permission audit log.

Reason privacy. The reason field is private. It is returned only for your own blocks, or when your key’s user has schedule management access; otherwise it is null in responses.

Validation. startDate and endDate must be plain YYYY-MM-DD calendar dates (timestamps are rejected), endDate must be on or after startDate, the inclusive range can span at most 366 days, and the target user must be an active member of your organisation. hoursUnavailable, when sent, must be greater than 0 and less than 24.

Conflict resolution. Creating or extending a block over days that already carry scheduled work does not fail. A whole-day block conflicts with any work on its days; a partial block conflicts only on days where the user’s total booked hours exceed the remaining capacity. The resolution field decides what happens to conflicting work: flag (the default) leaves the bookings in place and raises conflict warnings; replan moves the affected tasks so each partially blocked day holds at most the remaining hours and the excess moves to the next available working days, shifting dependent tasks with them and notifying the assignees. Use the preview endpoint first when you want to inspect the impact before writing.

List blocked-date ranges, defaulting to the key’s own user.

Query parameters

NameTypeRequiredDescription
userIdsstringNoComma-separated user IDs. Omit for your own blocks. Other users require schedule view access.
startDateISO 8601 dateNoOnly ranges that end on or after this date.
endDateISO 8601 dateNoOnly ranges that start on or before this date.

Example request

Terminal window
curl 'https://api.runnit.io/api/v1/blocked-dates?userIds=<user-uuid>&startDate=2027-09-01&endDate=2027-09-30' \
-H 'Authorization: Bearer rnk_your_key'

Response

A JSON array of blocked-date ranges in the shape shown above. reason is null unless the block is your own or your user has schedule management access.

Errors

  • 403 if you request another user’s blocks without schedule view access.

Get a read-only conflict report for a prospective block without creating or changing anything. Useful before a bulk leave sync, or before editing an existing block (send excludeBlockId).

Request body

FieldTypeRequiredDescription
userIdUUIDYesThe user the block is for.
startDateISO 8601 dateYesFirst blocked day.
endDateISO 8601 dateYesLast blocked day, inclusive.
hoursUnavailablenumberNoHours per day for a partial block. Omit for whole days.
excludeBlockIdUUIDNoWhen previewing an edit to an existing block, its ID. The preview then evaluates the values in this request as that block’s replacement instead of stacking them on top of the existing entry.

Example request

Terminal window
curl -X POST https://api.runnit.io/api/v1/blocked-dates/preview \
-H 'Authorization: Bearer rnk_your_key' \
-H 'Content-Type: application/json' \
-d '{"userId": "<user-uuid>", "startDate": "2027-09-06", "endDate": "2027-09-10", "hoursUnavailable": 4}'

Response

{
"conflicts": [
{
"taskId": "4be2d871-...",
"taskName": "Homepage concepts",
"projectId": "91c52864-...",
"projectName": "Solara website refresh",
"dates": ["2027-09-06", "2027-09-07"],
"hours": 9,
"bookingState": "confirmed",
"scheduleMode": "auto",
"replannable": true,
"overflowByDate": { "2027-09-06": 1, "2027-09-07": 0.5 }
}
],
"totalHours": 9,
"affectedTaskCount": 1,
"partialDayDetails": [
{
"date": "2027-09-06",
"blockedHours": 4,
"remainingCapacity": 4,
"bookedHours": 5,
"overflowHours": 1
}
]
}

bookingState is tentative, held, or confirmed. replannable is false when the task can’t be moved automatically.

For partial blocks, partialDayDetails breaks down each affected day: the blocked hours, the remaining capacity, the user’s booked hours, and the hours over the remaining capacity. Each conflict’s overflowByDate maps the dates where that task contributes to the overflow to the hours over. Both fields are absent for whole-day evaluations with no partial overflow.

Errors

  • 400 for invalid dates or a target who is not an active member.
  • 403 if the target is another user and you lack schedule management access.

Create a blocked-date range, then resolve any conflicts with scheduled work according to resolution.

Request body

FieldTypeRequiredDescription
userIdUUIDYesThe user the block is for. Your own user unless you have schedule management access.
startDateISO 8601 dateYesFirst blocked day.
endDateISO 8601 dateYesLast blocked day, inclusive. Range spans at most 366 days.
reasonstringNoFree text, up to 500 characters. Private to the user and schedule managers.
typestringNoleave, appointment, or other (default).
hoursUnavailablenumberNoHours per day for a partial block, greater than 0 and less than 24. Omit or send null for whole days. For half-day leave, send the hours directly.
resolutionstringNoflag (default) or replan.

Example request

Terminal window
curl -X POST https://api.runnit.io/api/v1/blocked-dates \
-H 'Authorization: Bearer rnk_your_key' \
-H 'Content-Type: application/json' \
-d '{"userId": "<user-uuid>", "startDate": "2027-09-06", "endDate": "2027-09-10", "type": "leave", "reason": "Synced from HR", "resolution": "replan"}'

Response

Returns 201 with the created block and the outcome of conflict resolution:

{
"block": { "id": "5b7d20aa-...", "startDate": "2027-09-06", "...": "..." },
"replan": {
"replanned": [
{
"taskId": "4be2d871-...",
"taskName": "Homepage concepts",
"fromStart": "2027-09-06",
"toStart": "2027-09-13",
"scheduledPastDueDate": false
}
],
"cascaded": { "shifted": [], "skipped": [], "evaluated": 1 },
"flagged": [],
"errors": [],
"residualConflicts": []
}
}

With resolution: "flag" (or when it is omitted), the response carries a conflicts report in the preview shape instead of replan. When there is no scheduled work on the dates, both are empty reports.

If the block is created but conflict resolution fails, the response includes a resolutionError message instead. The block exists at that point: retry with POST /api/v1/blocked-dates/:id/replan rather than re-creating it, which would duplicate the block.

Errors

  • 400 for invalid dates, a range over 366 days, or a target who is not an active organisation member.
  • 403 if the target is another user and you lack schedule management access.

Retry replanning whatever still sits on this block’s dates. Idempotent: safe to call again after a partial replan failure or a resolutionError.

Path parameters

NameTypeRequiredDescription
idUUIDYesBlocked-date range ID.

Example request

Terminal window
curl -X POST https://api.runnit.io/api/v1/blocked-dates/<block-id>/replan \
-H 'Authorization: Bearer rnk_your_key'

Response

{ "block": ..., "replan": ... } with the replan result in the shape shown above.

Errors

  • 403 if the block belongs to another user and you lack schedule management access.
  • 404 if the block does not exist or belongs to a different organisation.

Update a blocked-date range. Send only the fields you want to change. Days where the block became stricter (newly covered days, days that changed from partial to whole, or partial hours that increased) are put through conflict resolution using resolution (default flag); loosening a block frees capacity without re-resolving anything. A resolution failure is reported as resolutionError on the response, not as a failed update, and can be retried with the replan endpoint.

Request body

FieldTypeRequiredDescription
startDateISO 8601 dateNoNew first day.
endDateISO 8601 dateNoNew last day, inclusive.
reasonstringNoReplacement reason, up to 500 characters.
typestringNoleave, appointment, or other.
hoursUnavailablenumberNoOmit to leave the block’s hours unchanged. Send a number (greater than 0, less than 24) to set a partial block, or an explicit null to convert the block to whole days.
resolutionstringNoflag (default) or replan for newly covered dates.

Example request

Terminal window
curl -X PATCH https://api.runnit.io/api/v1/blocked-dates/<block-id> \
-H 'Authorization: Bearer rnk_your_key' \
-H 'Content-Type: application/json' \
-d '{"endDate": "2027-09-14", "resolution": "replan"}'

Response

{ "block": ... } with the updated range, plus conflicts, replan, or resolutionError when the change newly covered scheduled days.

Errors

  • 400 for invalid dates or a range over 366 days.
  • 403 if the block belongs to another user and you lack schedule management access.
  • 404 if the block does not exist or belongs to a different organisation.

Delete a blocked-date range. The days become available for scheduling again, but work that was replanned away does not move earlier automatically; replan the affected tasks if the freed time should be used.

Path parameters

NameTypeRequiredDescription
idUUIDYesBlocked-date range ID.

Example request

Terminal window
curl -X DELETE https://api.runnit.io/api/v1/blocked-dates/<block-id> \
-H 'Authorization: Bearer rnk_your_key'

Response

Returns 204 with no body.

Errors

  • 403 if the block belongs to another user and you lack schedule management access.
  • 404 if the block does not exist or belongs to a different organisation.

A working pattern is how long each of a person’s weekdays is. Somebody who works Mondays and half of Friday can say so, and the scheduler books work accordingly.

A pattern is the baseline capacity that leave is subtracted from, not another subtraction. Someone with no pattern is assumed to work the organisation’s daily capacity every weekday, which is how Runnit behaved before patterns existed.

A pattern looks like this:

{
"id": "f5d59857-...",
"organizationId": "8c2d41b7-...",
"userId": "d3a6f0c1-...",
"effectiveFrom": "2027-09-06",
"effectiveTo": null,
"hoursByWeekday": [8, 0, 0, 0, 4, 0, 0],
"note": "Synced from HR",
"weeklyHours": 12,
"createdAt": "2027-09-01T00:00:00.000Z",
"updatedAt": "2027-09-01T00:00:00.000Z",
"createdById": "d3a6f0c1-..."
}

hoursByWeekday is seven numbers starting on Monday. A zero means the person does not work that day. The example above is eight hours on Monday and four on Friday.

Effective dating. Setting a pattern does not edit the one before it. The previous pattern is closed on the day before the new effectiveFrom, so last month’s capacity does not change when someone goes part time. effectiveTo is null on the pattern currently in force.

Permissions. An API key acts as its owning user. Everyone can manage their own pattern. Reading another user’s requires schedule view access; setting or removing another user’s requires schedule management access. The note is private in the same way a blocked date’s reason is: it is returned for your own pattern, or to a caller with schedule management access, and is null otherwise.

Validation. hoursByWeekday must be exactly seven numbers, each from 0 to 24, and at least one must be greater than zero. A week of all zeroes is rejected, because that person could never be scheduled and nothing would say why. effectiveFrom must be a plain YYYY-MM-DD calendar date.

Read a person’s current pattern and their earlier ones. Defaults to the key’s own user.

Query parameters

NameTypeRequiredDescription
userIdUUIDNoWhose pattern to read. Defaults to the key’s own user.

Example request

Terminal window
curl 'https://api.runnit.io/api/v1/work-patterns?userId=<user-uuid>' \
-H 'Authorization: Bearer rnk_your_key'

Response

{
"success": true,
"userId": "d3a6f0c1-...",
"organizationDefaultHours": 8,
"current": { "...": "the pattern in force, or null" },
"patterns": [{ "...": "every pattern, newest first" }]
}

current is null when the person has no pattern, which means every weekday uses organizationDefaultHours.

Errors

  • 403 if you ask for another user without schedule view access.

The hours each date actually holds, for one or more people. This is the endpoint a planning integration usually wants: it answers “how much can this person do on this day” without you having to apply the pattern yourself.

Query parameters

NameTypeRequiredDescription
fromISO 8601 dateYesFirst date in the window.
toISO 8601 dateYesLast date in the window, inclusive. At most 400 days after from.
userIdsComma-separated UUIDsNoWhose capacity to return. Defaults to the key’s own user. Up to 200 users.

Example request

Terminal window
curl 'https://api.runnit.io/api/v1/work-patterns/capacity?userIds=<user-uuid>&from=2027-09-06&to=2027-09-12' \
-H 'Authorization: Bearer rnk_your_key'

Response

{
"success": true,
"organizationDefaultHours": 8,
"overrides": {
"d3a6f0c1-...": {
"2027-09-07": 0,
"2027-09-08": 0,
"2027-09-09": 0,
"2027-09-10": 4,
"2027-09-11": 0,
"2027-09-12": 0
}
}
}

overrides holds only the dates that differ from organizationDefaultHours. Any date not listed uses the default, which is why Monday is absent from the example above. A person with no pattern has no entry at all.

Errors

  • 400 if to is before from, or the range covers more than 400 days. The message says how many days you asked for.
  • 403 if you ask for another user without schedule view access.

Set a pattern from a date onwards.

Body parameters

NameTypeRequiredDescription
userIdUUIDNoWhose pattern to set. Defaults to the key’s own user.
effectiveFromISO 8601 dateYesFirst day the new pattern applies.
hoursByWeekdayArray of 7 numbersYesHours per weekday, starting Monday. 0 means the person does not work that day.
noteStringNoA private note, such as where the change came from.

Example request

Terminal window
curl -X POST https://api.runnit.io/api/v1/work-patterns \
-H 'Authorization: Bearer rnk_your_key' \
-H 'Content-Type: application/json' \
-d '{
"userId": "<user-uuid>",
"effectiveFrom": "2027-09-06",
"hoursByWeekday": [8, 0, 0, 0, 4, 0, 0],
"note": "Synced from HR"
}'

Response

201 with the created pattern under pattern.

Errors

  • 400 if hoursByWeekday is not seven valid numbers, or is all zeroes.
  • 403 if you set another user’s pattern without schedule management access.

Remove a pattern. Removing the one currently in force reopens the pattern before it, so nobody silently drops back to the organisation default when an earlier pattern exists.

Path parameters

NameTypeRequiredDescription
idUUIDYesPattern ID.

Example request

Terminal window
curl -X DELETE https://api.runnit.io/api/v1/work-patterns/<pattern-id> \
-H 'Authorization: Bearer rnk_your_key'

Response

{ "success": true }

Errors

  • 403 if the pattern belongs to another user and you lack schedule management access.
  • 404 if the pattern does not exist or belongs to a different organisation.

Runnit can read a person’s Google or Outlook calendar and can put their scheduled work into it. The endpoints below let you see the state of that. See Calendar Sync for what the feature does.

Microsoft connections use the main Outlook calendar. The same read routes and permissions apply to Google and Microsoft through REST and MCP call_public_api. Calendar consent and configuration still require the browser.

busyOnlyIsTransportLevel is false for both Microsoft account types. Microsoft may transmit meeting details; Runnit discards them for availability-only sync. This field and privacy behaviour are the same through REST and MCP.

For Microsoft, live notifications require an approved calendar read permission; an availability-only connection can remain healthy through regular polling. notificationsLive: false alone does not mean the connection has failed. Inspect the last read time and errors as well. Work written to Outlook shares the main calendar, but only Runnit’s own entries can change its scheduled work.

The calendars the key’s own user has connected, and what this workspace does with each. Scoped to that user: nobody reads anybody else’s connections through this, including an administrator.

The meetings behind the schedule, already filtered by what this workspace is allowed to see.

Query parameters

NameTypeRequiredDescription
fromISO 8601 dateYesFirst date.
toISO 8601 dateYesLast date, inclusive.
userIdsComma-separated UUIDsNoDefaults to the key’s own user. Up to 100. Reading another person needs schedule view access.

Each event carries startsAt, endsAt, isAllDay and showAs. title is null when the workspace reads availability only, and titleWithheld is true so you can tell that apart from a meeting with no name.

The response also carries affectsCapacity, which says whether these meetings currently reduce the hours Runnit will book. Read it rather than assuming.

Per connection: when it was last read and last written, the last error, how many calendars are in error, how stale it is, and whether live change notifications are running.

That last field is the one worth alerting on. A connection whose notifications have lapsed still syncs on a fifteen-minute sweep, so the only symptom is latency and nothing reports it.

Pass scope=organization to see everyone, which needs schedule view or calendar policy access.

Counts of what has fallen out of step across the workspace: entries Runnit believes it created that are no longer there, notification-capable calendars with no live notifications, accounts needing reconnection, mirrored meetings past the retention window, and stale capacity rollups. Requires calendar policy access.

The workspace ceiling: whether sync is available at all, whether meeting names may be read, and whether meetings may reduce capacity. Changing it is an in-app operation.

Schedules are built from task assignments. Create and reschedule tasks through the Projects API, and update task status through the Tasks API.