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
| Field | Type | Description |
|---|---|---|
id | ID! | Unique identifier |
name | String! | Project name |
description | String(nullable) | Free-text description |
color | String(nullable) | Hex color used for display |
status | ProjectStatusEnum! | Current status (TODO, IN_PROGRESS, DONE) |
priority | ProjectPriorityEnum! | Priority (LOW, MEDIUM, HIGH, URGENT) |
startDate | Date(nullable) | Scheduled start date |
endDate | Date(nullable) | Scheduled end / due date |
dueDate | Date(nullable) | Alias of endDate |
sourceTemplateId | ID(nullable) | Template the project was created from, if any |
createdAt | Date! | Creation timestamp |
updatedAt | Date! | Last update timestamp |
deletedAt | Date(nullable) | Soft-delete timestamp |
deleted | Boolean(nullable) | Whether the project is soft-deleted |
Relations are resolved on demand — request only what you need:
| Field | Type | Description |
|---|---|---|
customer | Customer! | Customer the project belongs to |
category | ProjectCategory(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 |
taskProgress | ProjectTaskProgress! | Completed vs. total appointment counts |
address | String(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.