Projects

Organize multi-visit work for a customer into a tracked project

Overview

A project groups related work for a single customer — a renovation, a green-pool recovery, a seasonal opening — under one record with a status, priority, schedule, assigned workers, notes, and the appointments and services that make it up. Projects can be created from a project template to generate their appointments automatically.

Every project belongs to a customer and a category. Progress is tracked from its appointments via the taskProgress field.

The Project Object

FieldTypeDescription
idID!Unique identifier
nameString!Project name
descriptionString(nullable)Free-text description
colorString(nullable)Hex color used for display
statusProjectStatusEnum!Current status (TODO, IN_PROGRESS, DONE)
priorityProjectPriorityEnum!Priority (LOW, MEDIUM, HIGH, URGENT)
startDateDate(nullable)Scheduled start date
endDateDate(nullable)Scheduled end / due date
dueDateDate(nullable)Alias of endDate
sourceTemplateIdID(nullable)Template the project was created from, if any
createdAtDate!Creation timestamp
updatedAtDate!Last update timestamp
deletedAtDate(nullable)Soft-delete timestamp
deletedBoolean(nullable)Whether the project is soft-deleted

Relations are resolved on demand — request only what you need:

FieldTypeDescription
customerCustomer!Customer the project belongs to
categoryProjectCategory(nullable)Project category
workers[ProjectWorker!]!Assigned workers (each with a primary flag and user)
appointments[AppointmentIdentifier!](nullable)Appointments and recurring rules belonging to the project
services[Service!](nullable)Services linked to the project
notes[ProjectNote!]!Project notes
taskProgressProjectTaskProgress!Completed vs. total appointment counts
addressString(nullable)Formatted address of the project's customer

Status & Priority

status is one of TODO, IN_PROGRESS, DONE. priority is one of LOW, MEDIUM, HIGH, URGENT.

List Projects

Retrieve a cursor-paginated list with infiniteProjects. Pass a selector to filter by name, status, date ranges, a linked appointment or service, or a free-text search; and a sort argument (by name, status, startDate, endDate, createdAt, or updatedAt).

query InfiniteProjects($first: Int!, $after: String, $selector: ProjectsSelector) {
  infiniteProjects(first: $first, after: $after, selector: $selector) {
    edges {
      node {
        id
        name
        status
        priority
        startDate
        endDate
        customer {
          id
          firstName
          lastName
        }
        taskProgress {
          completed
          total
        }
      }
      cursor
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

# Variables
{
  "first": 20,
  "selector": {
    "filters": { "status": { "equals": "IN_PROGRESS" } },
    "search": "renovation"
  }
}

Response:

{
  "data": {
    "infiniteProjects": {
      "edges": [
        {
          "node": {
            "id": "prj_8a1c3e5b7d9f",
            "name": "Pool Resurfacing",
            "status": "IN_PROGRESS",
            "priority": "HIGH",
            "startDate": "2026-06-01",
            "endDate": "2026-06-20",
            "customer": { "id": "cus_2b4d6f8a1c3e", "firstName": "Jordan", "lastName": "Rivera" },
            "taskProgress": { "completed": 3, "total": 8 }
          },
          "cursor": "eyJpZCI6InByal84YTFjM2U1YjdkOWYifQ"
        }
      ],
      "pageInfo": { "hasNextPage": true, "endCursor": "eyJpZCI6InByal84YTFjM2U1YjdkOWYifQ" }
    }
  }
}

Create a Project

createProject requires a name, customerId, and categoryId. You may assign workers via assigneeIds. To generate appointments from a template, pass templateId together with a startDate, and optionally perAppointmentOverrides to tweak individual generated appointments.

mutation CreateProject($input: CreateProjectInput!) {
  createProject(input: $input) {
    id
    name
    status
    priority
  }
}

# Variables
{
  "input": {
    "name": "Pool Resurfacing",
    "customerId": "cus_2b4d6f8a1c3e",
    "categoryId": "pcat_1a2b3c4d",
    "priority": "HIGH",
    "startDate": "2026-06-01",
    "endDate": "2026-06-20",
    "assigneeIds": ["usr_5b7d9f1a3c5e"]
  }
}

Update a Project

updateProject takes the project id and an UpdateProjectInput — every field is optional. Providing assigneeIds replaces the full set of workers. You can also reassign appointments to or from the project via appointments, and edit template-generated appointments via appointmentUpdates.

mutation UpdateProject($id: ID!, $input: UpdateProjectInput!) {
  updateProject(id: $id, input: $input) {
    id
    name
    status
  }
}

# Variables
{
  "id": "prj_8a1c3e5b7d9f",
  "input": { "status": "DONE" }
}

Delete a Project

deleteProject soft-deletes the project and returns it. Linked services are unlinked, not deleted.

mutation DeleteProject($id: ID!) {
  deleteProject(id: $id) {
    id
    deleted
  }
}

Linking Services

Attach an existing service to a project, or detach it. Both return the affected Service.

mutation LinkService {
  linkServiceToProject(serviceId: "svc_001", projectId: "prj_8a1c3e5b7d9f") {
    id
  }
}

mutation UnlinkService {
  unlinkServiceFromProject(serviceId: "svc_001") {
    id
  }
}

Project Categories

Categories are required on every project. List them with projectCategories, and manage them with createProjectCategory and updateProjectCategory.

query {
  projectCategories {
    id
    name
  }
}

mutation {
  createProjectCategory(input: { name: "Renovations" }) {
    id
    name
  }
}

Project Notes

Attach free-text notes to a project with createProjectNote, and edit or remove them with updateProjectNote and deleteProjectNote.

mutation AddNote {
  createProjectNote(input: { projectId: "prj_8a1c3e5b7d9f", body: "Customer prefers morning visits." }) {
    id
    body
    createdAt
  }
}

Bulk & Export Operations

Projects have no per-project bulk endpoints. Acting on many records at once — for the appointments or services within projects — is handled asynchronously through the Bulk Operations API, which runs the work in the background and reports progress you can poll. See the Bulk Operations guide for the full workflow.

Authorization

Project queries and mutations require an authenticated session — any valid API key works. There is no dedicated project scope; access is scoped to the organization the key belongs to.