Appointments
Manage scheduled service appointments for customers
Overview
Appointments represent scheduled service visits. They can be assigned to workers, linked to customers, and include inventory items. Appointments track status through three stages: OPEN, ASSIGNED, and COMPLETE. Recurring appointments are supported through the identifier system, which combines an ID with optional recurring date and rule references.
Appointment Identifier
Appointments use an AppointmentIdentifierInput object for lookups instead of a simple ID. This supports both regular and recurring appointments. The identifier contains:
| Field | Type | Description |
|---|---|---|
id | ID(nullable) | Appointment ID |
recurringDate | Date(nullable) | Date of the recurring occurrence |
recurringRuleId | ID(nullable) | Recurring rule ID |
version | Int(nullable) | Version number |
List Appointments
Retrieve a cursor-paginated list of appointments with optional filtering. Uses AppointmentsSelector for filtering by date range, status, priority, workers, and more. Maximum of 100 items per request. Both startDate and endDate must be provided together.
query PaginatedAppointments(
$first: Int!,
$after: String,
$selector: AppointmentsSelector
) {
paginatedAppointments(first: $first, after: $after, selector: $selector) {
edges {
cursor
node {
id
date
duration
status
priority
color
notes
pinned
customer {
id
firstName
lastName
}
workers {
id
firstName
lastName
}
primaryWorker {
id
firstName
lastName
}
serviceType {
id
name
}
billingStatus {
id
name
}
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
# Variables
{
"first": 20,
"after": null,
"selector": {
"startDate": "2024-01-01",
"endDate": "2024-01-31",
"filters": {
"status": "OPEN"
},
"includePinned": true
}
}Response:
{
"data": {
"paginatedAppointments": {
"edges": [
{
"cursor": "YXBwb2ludG1lbnRfMTIz",
"node": {
"id": "clxyz1234567890",
"date": "2024-01-15T09:00:00.000Z",
"duration": 60,
"status": "ASSIGNED",
"priority": "MEDIUM",
"color": "#4A90D9",
"notes": null,
"pinned": false,
"customer": {
"id": "clxyz_cust_001",
"firstName": "John",
"lastName": "Smith"
},
"workers": [
{
"id": "clxyz_worker_001",
"firstName": "Mike",
"lastName": "Johnson"
}
],
"primaryWorker": {
"id": "clxyz_worker_001",
"firstName": "Mike",
"lastName": "Johnson"
},
"serviceType": {
"id": "clxyz_svc_001",
"name": "Weekly Cleaning"
},
"billingStatus": {
"id": "clxyz_bs_001",
"name": "Unbilled"
}
}
}
],
"pageInfo": {
"hasNextPage": true,
"hasPreviousPage": false,
"startCursor": "YXBwb2ludG1lbnRfMTIz",
"endCursor": "YXBwb2ludG1lbnRfMTIz"
}
}
}
}Infinite Appointments
Forward-only cursor pagination for infinite scroll UIs. Uses the same AppointmentsSelector as paginatedAppointments. Maximum of 100 items per request.
query InfiniteAppointments(
$first: Int!,
$after: String,
$selector: AppointmentsSelector
) {
infiniteAppointments(first: $first, after: $after, selector: $selector) {
edges {
cursor
node {
id
date
duration
status
customer {
id
firstName
lastName
}
workers {
id
firstName
lastName
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
# Variables
{
"first": 20,
"after": null,
"selector": {
"startDate": "2024-01-01",
"endDate": "2024-03-31",
"filters": {
"status": "COMPLETE",
"priority": "HIGH"
}
}
}Response:
{
"data": {
"infiniteAppointments": {
"edges": [
{
"cursor": "YXBwb2ludG1lbnRfNDU2",
"node": {
"id": "clxyz_apt_456",
"date": "2024-02-10T14:00:00.000Z",
"duration": 45,
"status": "COMPLETE",
"customer": {
"id": "clxyz_cust_002",
"firstName": "Sarah",
"lastName": "Williams"
},
"workers": [
{
"id": "clxyz_worker_002",
"firstName": "James",
"lastName": "Davis"
}
]
}
}
],
"pageInfo": {
"hasNextPage": true,
"endCursor": "YXBwb2ludG1lbnRfNDU2"
}
}
}
}Get Single Appointment
Retrieve a single appointment by its identifier. Use the appointment query with an AppointmentIdentifierInput.
query Appointment($identifier: AppointmentIdentifierInput!) {
appointment(identifier: $identifier) {
id
date
duration
status
priority
color
notes
privateNotes
servicePrice
serviceQuantity
pinned
customer {
id
firstName
lastName
email
phone
}
workers {
id
firstName
lastName
}
primaryWorker {
id
firstName
lastName
}
serviceType {
id
name
}
inventoryItems {
inventoryId
quantity
price
}
billingStatus {
id
name
}
serviceStatus {
id
name
}
identifier {
id
recurringDate
recurringRuleId
version
}
recurringNext
recurringDate
createdAt
updatedAt
}
}
# Variables
{
"identifier": {
"id": "clxyz1234567890"
}
}Response:
{
"data": {
"appointment": {
"id": "clxyz1234567890",
"date": "2024-01-15T09:00:00.000Z",
"duration": 60,
"status": "ASSIGNED",
"priority": "MEDIUM",
"color": "#4A90D9",
"notes": "Check chlorine levels",
"privateNotes": "Customer prefers back gate entry",
"servicePrice": 7500,
"serviceQuantity": 1.0,
"pinned": false,
"customer": {
"id": "clxyz_cust_001",
"firstName": "John",
"lastName": "Smith",
"email": "john.smith@example.com",
"phone": "555-0123"
},
"workers": [
{
"id": "clxyz_worker_001",
"firstName": "Mike",
"lastName": "Johnson"
}
],
"primaryWorker": {
"id": "clxyz_worker_001",
"firstName": "Mike",
"lastName": "Johnson"
},
"serviceType": {
"id": "clxyz_svc_001",
"name": "Weekly Cleaning"
},
"inventoryItems": [
{
"inventoryId": "clxyz_inv_001",
"quantity": 2.0,
"price": 1200
}
],
"billingStatus": {
"id": "clxyz_bs_001",
"name": "Unbilled"
},
"serviceStatus": {
"id": "clxyz_ss_001",
"name": "Pending"
},
"identifier": {
"id": "clxyz1234567890",
"recurringDate": null,
"recurringRuleId": null,
"version": 1
},
"recurringNext": false,
"recurringDate": null,
"createdAt": "2024-01-01T10:00:00.000Z",
"updatedAt": "2024-01-10T14:30:00.000Z"
}
}
}Create Appointment
Create a new appointment for a customer. The data argument accepts a CreateAppointmentInput. Note that inventoryItems is required but can be an empty array [] when no inventory is needed. Dates should be in ISO 8601 format.
Assigning technicians: pass the technician user IDs in the workers array to assign them to the appointment, and optionally set primaryWorker to one of those same IDs to mark the primary technician. Both accept user IDs (the id field on a user), which you can look up with the Users API (for example infiniteUsers or lookupUser). Pass an empty array [] to create an unassigned appointment. Assigning at least one worker moves the appointment from OPEN to ASSIGNED.
mutation CreateAppointment($data: CreateAppointmentInput!) {
createAppointment(data: $data) {
id
date
duration
status
priority
color
customer {
id
firstName
lastName
}
workers {
id
firstName
lastName
}
serviceType {
id
name
}
inventoryItems {
inventoryId
quantity
price
}
}
}
# Variables
{
"data": {
"date": "2024-02-01T10:00:00.000Z",
"duration": 90,
"customer": "clxyz_cust_001",
"serviceType": "clxyz_svc_001",
"workers": ["clxyz_worker_001"],
"inventoryItems": [
{
"inventoryId": "clxyz_inv_001",
"quantity": 2.0,
"price": 1200
}
],
"notes": "Annual pool inspection",
"color": "#4A90D9",
"status": "OPEN",
"priority": "MEDIUM",
"primaryWorker": "clxyz_worker_001",
"servicePrice": 15000,
"serviceQuantity": 1.0
}
}Response:
{
"data": {
"createAppointment": {
"id": "clxyz_apt_new",
"date": "2024-02-01T10:00:00.000Z",
"duration": 90,
"status": "OPEN",
"priority": "MEDIUM",
"color": "#4A90D9",
"customer": {
"id": "clxyz_cust_001",
"firstName": "John",
"lastName": "Smith"
},
"workers": [
{
"id": "clxyz_worker_001",
"firstName": "Mike",
"lastName": "Johnson"
}
],
"serviceType": {
"id": "clxyz_svc_001",
"name": "Pool Inspection"
},
"inventoryItems": [
{
"inventoryId": "clxyz_inv_001",
"quantity": 2.0,
"price": 1200
}
]
}
}
}Update Appointment
Update an existing appointment. Uses an AppointmentIdentifierInput to locate the appointment and an UpdateAppointmentInput with the fields to update. All fields in the update input are optional.
Reassigning technicians: send a workers array to replace the appointment's current technicians with the given set of user IDs, and set primaryWorker to one of those IDs to change the primary technician. As with create, IDs are user IDs you can look up via the Users API. Because workers replaces the whole set, pass [] to unassign everyone. Omitting workers (or primaryWorker) leaves the existing assignment unchanged, so you can update other fields without disturbing the crew.
mutation UpdateAppointment(
$identifier: AppointmentIdentifierInput!,
$data: UpdateAppointmentInput!
) {
updateAppointment(identifier: $identifier, data: $data) {
id
date
duration
status
priority
notes
workers {
id
firstName
lastName
}
updatedAt
}
}
# Variables
{
"identifier": {
"id": "clxyz1234567890"
},
"data": {
"status": "COMPLETE",
"notes": "Service completed, chlorine levels adjusted",
"priority": "LOW",
"workers": ["clxyz_worker_002"],
"primaryWorker": "clxyz_worker_002"
}
}Response:
{
"data": {
"updateAppointment": {
"id": "clxyz1234567890",
"date": "2024-01-15T09:00:00.000Z",
"duration": 60,
"status": "COMPLETE",
"priority": "LOW",
"notes": "Service completed, chlorine levels adjusted",
"workers": [
{
"id": "clxyz_worker_001",
"firstName": "Mike",
"lastName": "Johnson"
}
],
"updatedAt": "2024-01-15T10:15:00.000Z"
}
}
}Delete Appointment
Delete an appointment using its identifier. Returns a StatusEntity.
mutation DeleteAppointment($identifier: AppointmentIdentifierInput!) {
deleteAppointment(identifier: $identifier) {
success
message
}
}
# Variables
{
"identifier": {
"id": "clxyz1234567890"
}
}Response:
{
"data": {
"deleteAppointment": {
"success": true,
"message": "Appointment deleted successfully"
}
}
}Bulk & Export Operations
Creating, updating, deleting, exporting, emailing, texting, or printing many appointments at once is handled asynchronously through the Bulk Operations API. These jobs run in the background and report progress you can poll. See the Bulk Operations guide for the full workflow.
Authorization
Appointment operations require the following permissions:
| Field | Type | Description |
|---|---|---|
appointment (query) | Permission | read:own:appointment or read:all:appointment |
paginatedAppointments | Permission | read:own:appointment or read:all:appointment |
infiniteAppointments | Permission | read:own:appointment or read:all:appointment |
createAppointment | Permission | write:appointment |
updateAppointment | Permission | write:appointment |
deleteAppointment | Permission | delete:appointment |
Field Reference
| Field | Type | Description |
|---|---|---|
id | ID! | Unique identifier |
date | Date! | Appointment date (ISO 8601 format) |
duration | Number! | Duration in minutes |
notes | String(nullable) | Notes visible to the customer |
privateNotes | String(nullable) | Internal notes not visible to the customer |
color | String! | Color code for calendar display |
servicePrice | Int(nullable) | Price of the service in cents |
serviceQuantity | Float(nullable) | Quantity of the service |
status | AppointmentStatusEnum! | Current status (OPEN, ASSIGNED, COMPLETE) |
priority | AppointmentPriorityEnum! | Priority level (LOW, MEDIUM, HIGH, SPECIAL) |
pinned | Boolean(nullable) | Whether the appointment is pinned |
workers | [AppointmentWorkerEntity]! | Workers assigned to this appointment |
primaryWorker | AppointmentWorkerEntity(nullable) | Primary worker for this appointment |
customer | CustomerEntity | Associated customer (resolved) |
serviceType | TypeEntity(nullable) | Type of service being performed |
inventoryItems | [AppointmentInventoryItemEntity](nullable) | Inventory items used in this appointment |
billingStatus | BillingStatusEntity(nullable) | Billing status of the appointment |
serviceStatus | ServiceStatusEntity(nullable) | Service status of the appointment |
identifier | AppointmentIdentifierEntity! | Unique identifier object (supports recurring appointments) |
recurringNext | Boolean(nullable) | Whether this is the next occurrence of a recurring appointment |
recurringDate | Date(nullable) | Date of the recurring occurrence |
createdAt | Date! | Creation timestamp |
updatedAt | Date! | Last update timestamp |