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:

FieldTypeDescription
idID(nullable)Appointment ID
recurringDateDate(nullable)Date of the recurring occurrence
recurringRuleIdID(nullable)Recurring rule ID
versionInt(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:

FieldTypeDescription
appointment (query)Permissionread:own:appointment or read:all:appointment
paginatedAppointmentsPermissionread:own:appointment or read:all:appointment
infiniteAppointmentsPermissionread:own:appointment or read:all:appointment
createAppointmentPermissionwrite:appointment
updateAppointmentPermissionwrite:appointment
deleteAppointmentPermissiondelete:appointment

Field Reference

FieldTypeDescription
idID!Unique identifier
dateDate!Appointment date (ISO 8601 format)
durationNumber!Duration in minutes
notesString(nullable)Notes visible to the customer
privateNotesString(nullable)Internal notes not visible to the customer
colorString!Color code for calendar display
servicePriceInt(nullable)Price of the service in cents
serviceQuantityFloat(nullable)Quantity of the service
statusAppointmentStatusEnum!Current status (OPEN, ASSIGNED, COMPLETE)
priorityAppointmentPriorityEnum!Priority level (LOW, MEDIUM, HIGH, SPECIAL)
pinnedBoolean(nullable)Whether the appointment is pinned
workers[AppointmentWorkerEntity]!Workers assigned to this appointment
primaryWorkerAppointmentWorkerEntity(nullable)Primary worker for this appointment
customerCustomerEntityAssociated customer (resolved)
serviceTypeTypeEntity(nullable)Type of service being performed
inventoryItems[AppointmentInventoryItemEntity](nullable)Inventory items used in this appointment
billingStatusBillingStatusEntity(nullable)Billing status of the appointment
serviceStatusServiceStatusEntity(nullable)Service status of the appointment
identifierAppointmentIdentifierEntity!Unique identifier object (supports recurring appointments)
recurringNextBoolean(nullable)Whether this is the next occurrence of a recurring appointment
recurringDateDate(nullable)Date of the recurring occurrence
createdAtDate!Creation timestamp
updatedAtDate!Last update timestamp