type Address { street1: String! street2: String! city: String state: String county: String zip: String! country: String! } input AddressFilter { street1: StringFilter street2: StringFilter city: StringFilter state: StringFilter county: StringFilter zip: StringFilter country: StringFilter } input AddressSort { street1: SortOrder street2: SortOrder city: SortOrder state: SortOrder county: SortOrder zip: SortOrder country: SortOrder } input AddressInput { street1: String street2: String city: String state: String country: String zip: String } """ Intent hint that scopes which schema types and operations are pre-loaded for the AI. Multiple intents can be provided — their schema slices are unioned. When in doubt, or when the question spans multiple domains, use ALL. """ enum AiIntent { """ Actions performed by office or admin staff *about* a job: scheduling, editing job metadata (title, category, customer, color, contract), assigning crews, managing line items, route planning, and calendar management. If someone is looking at a job board or dispatcher screen and wants to change something about a job, this is the right intent. """ DISPATCH """ Actions performed by a technician *on-site during a visit*: updating job status (open/close/skip), capturing before/after photos, filling out visit forms, starting/stopping timers, and clocking in or out. If someone is standing at a job site with a phone, this is the right intent. """ FIELD_WORK """Invoices, line items, and customer payments.""" INVOICING """Estimates / quotes, their line items, and lifecycle status.""" ESTIMATING """Customers and their properties.""" CUSTOMERS """Services catalog, item pricing, and packages.""" PRICING """Users, crews, time tracking, and clock-in.""" TEAM """ Aggregate report queries across events, invoices, estimates, payments, and SMS. """ REPORTING """ SMS and messaging — inbound/outbound conversations with customers, delivery status, and messaging reports. """ COMMUNICATIONS """ Loads the full cross-domain schema slice. Use this when intent is unknown, the question spans multiple domains, or you are unsure whether DISPATCH or FIELD_WORK (or any other intent) is the right fit. Prefer ALL over guessing wrong. """ ALL } enum AiResponseFormat { """Default. AI response is formatted as Markdown.""" MARKDOWN """Plain text, suitable for SMS, email body, or legacy views.""" PLAIN_TEXT } input AiContextObjectInput { objectType: ObjectType! objectIds: [SafeInt!] } input AiContextInput { objects: [AiContextObjectInput!] intents: [AiIntent!] dateRange: DateRangeInput sessionId: String responseFormat: AiResponseFormat metadata: JSON } input DateRangeInput { start: DateTime! end: DateTime! } enum AiMessageSenderType { USER ASSISTANT } type AiMessage { id: SafeInt! senderType: AiMessageSenderType! text: String! createdAt: DateTime! } input AiMessageSort { id: SortOrder senderType: SortOrder createdAt: SortOrder } type AiChat { id: SafeInt! messages(orderBy: [AiMessageSort!], take: SafeInt, skip: SafeInt): [AiMessage!]! createdAt: DateTime! updatedAt: DateTime! } input AiChatSort { id: SortOrder createdAt: SortOrder updatedAt: SortOrder } type ApiTrial { active: Boolean! trialStartedAt: DateTime trialEndsAt: Date daysRemaining: Int! } input ApiTrialFilter { trialStartedAt: DateTimeFilter trialEndsAt: DateTimeFilter isActive: Boolean } enum AppEventType { ADMIN_CREATED ADMIN_DELETED ADMIN_UPDATED COMPANY_CREATED COMPANY_UPDATED CUSTOMER_ADDED_RATING CUSTOMER_CREATED CUSTOMER_CREDIT_ADJUSTED CUSTOMER_MARKED_INACTIVE CUSTOMER_PAYMENT_DECLINED @deprecated(reason: "Use PAYMENT_DECLINED") CUSTOMER_PAYMENT_METHOD_ADDED CUSTOMER_PAYMENT_METHOD_REMOVED CUSTOMER_PAYMENT_RECEIVED @deprecated(reason: "Use PAYMENT_RECEIVED") CUSTOMER_SMS_RECEIVED CUSTOMER_STATUS_UPDATED CUSTOMER_UPDATED ESTIMATE_ACCEPTED ESTIMATE_CHANGES_REQUESTED ESTIMATE_CREATED ESTIMATE_DECLINED ESTIMATE_DELETED ESTIMATE_DEPOSIT_RECEIVED ESTIMATE_DRAFTED ESTIMATE_INVOICED ESTIMATE_LINE_ADDED ESTIMATE_LINE_CREATED ESTIMATE_LINE_DELETED ESTIMATE_LINE_REMOVED ESTIMATE_LINE_UPDATED ESTIMATE_NOT_RESPONDED ESTIMATE_PENDING ESTIMATE_RESPONDED ESTIMATE_SENT ESTIMATE_SERVICE ESTIMATE_UPDATED ESTIMATE_VIEWED EVENT_ASSIGNED EVENT_CLOSED EVENT_COMPLETED EVENT_CREATED EVENT_DELETED EVENT_OPENED EVENT_SKIPPED EVENT_TIMER_STARTED EVENT_TIMER_STOPPED EVENT_UPDATED INVOICE_CREATED INVOICE_DELETED INVOICE_SENT INVOICE_UPDATED INVOICE_VIEWED MCP_QUERY_EXECUTED PAYMENT_DECLINED PAYMENT_RECEIVED RATING_CREATED ROUTE_CREATED SUBSCRIPTION_CANCELLED SUBSCRIPTION_CREATED SUBSCRIPTION_DOWNGRADED SUBSCRIPTION_RESUMED SUBSCRIPTION_UNCANCELLED SUBSCRIPTION_UPGRADED TAG_ADDED TAG_REMOVED TIME_ENTRY_CREATED TIME_ENTRY_DELETED TIME_ENTRY_UPDATED USER_CREATED USER_DELETED USER_IMPERSONATED USER_UPDATED WORK_REQUEST_CREATED } enum AppEventActorType { ADMIN CUSTOMER SYSTEM USER } type AppEventActor { id: SafeInt name: String email: String } input SignUpInput { email: String! password: String! recaptchaToken: String! } type SignUpResult { userId: SafeInt! companyId: SafeInt! accessToken: String! refreshToken: String! tokenType: String! expiresIn: Int! } type AuthResponse { accessToken: String! refreshToken: String tokenType: String! expiresIn: Int } enum RegistrationSource { GOOGLE PODCAST REFERRAL SEARCH SOCIAL_MEDIA YOUTUBE_MIKE_ANDES YOUTUBE_HOME_SERVICE_MILLIONAIRE INSTAGRAM FACEBOOK EMAIL FREE_COURSES REFERRAL_WORD_OF_MOUTH OTHER } input UserContactInput { firstName: String! lastName: String! } input CompanyContactInput { name: String! website: String phone: String source: RegistrationSource sourceOther: String sourceStimulus: String street1: String! street2: String city: String! state: String! zip: String! country: String! latitude: Float longitude: Float } type Query { """ Checks whether a user with the given email address already exists. Public endpoint — no authentication required. """ userExists(email: String!): Boolean! """Health check query. Returns 'pong' to confirm the API is reachable.""" ping: String! """ Returns the Stripe configuration and subscription data for the current company. """ stripe: Stripe """ The authenticated company's subscription, tier, and current billing period usage. """ subscription: CompanySubscription aiChat(id: SafeInt!): AiChat! aiChats(orderBy: [AiChatSort!], take: SafeInt = 1000, skip: SafeInt): [AiChat!]! """ List areas for the authenticated company. Defaults to live (non-deleted) areas in list order (sortOrder, then id). Pass where: { isDeleted: true } for soft-deleted areas only. """ areas(where: AreaFilter = {isDeleted: false}, orderBy: [AreaSort!] = [{sortOrder: asc}, {id: asc}], take: SafeInt, skip: SafeInt): [Area!]! """Fetch a single area by ID. Throws if not found.""" area(id: SafeInt!): Area! """ Accessible to all authenticated company users. Returns the current user's company with its settings, plan, and billing information. """ company: Company! """ Returns all crews for the company. Defaults to non-deleted crews. Pass where: { isDeleted: true } to include deleted crews. Requires crews.READ. """ crews(where: CrewFilter = {isDeleted: false}, skip: SafeInt, take: SafeInt, orderBy: [CrewSort!]): [Crew] """ Returns a single crew by ID, including soft-deleted records. Throws NotFoundException if not found. Requires crews.READ. """ crew(crewId: SafeInt!): Crew! """ Company custom-field definitions for column pickers. Always limited to event-resolvable owner types (EVENT, CUSTOMER, ASSET, ESTIMATE, INVOICE) and type != NONE. Optional objectType narrows within that set. Ordered by objectType, then name, then id. """ dynamicCustomFields(where: DynamicCustomFieldFilter): [DynamicCustomField!]! """ Aggregate sum per numeric custom-field definition over the events matching `where` (same authorization as the `events` query). Non-summable fieldIds are omitted. """ eventCustomFieldTotals(fieldIds: [SafeInt!]!, where: EventFilter): [CustomFieldTotal!]! """ Aggregate customer data grouped by the dimensions you select (customerTypeId, sourceId, status, isDeleted, country, state, city, zip). If no dimension fields are selected, returns a single row with company-wide totals. Useful for geographic breakdowns, customer type distribution, lead source analysis, and acquisition trends. Requires customers.READ. """ customerReport(where: CustomerFilter, orderBy: [CustomerReportOrderBy!], take: SafeInt, skip: SafeInt): [CustomerReportRow!]! """ Returns all customers for the company. Defaults to non-deleted customers. Pass where: { isDeleted: true } to include deleted customers. Requires customers.READ. """ customers(where: CustomerFilter = {isDeleted: false}, skip: SafeInt, take: SafeInt = 1000, orderBy: [CustomerSort!]): [Customer!]! """ Returns a single customer by ID, including soft-deleted records. Throws NotFoundException if not found. Requires customers.READ. """ customer(customerId: SafeInt!): Customer! """ Returns all customer types available to this company — includes both company-specific types and global/system types (companyId = null). Requires customers.READ. """ customerTypes: [CustomerType!]! """ Returns all customer sources available to this company — includes both company-specific sources and global/system sources (companyId = null). Only returns visible sources. Requires customers.READ. """ customerSources: [CustomerSource!]! """ Returns the company's email templates, ordered by name. Requires emails.CREATE permission. """ emailTemplates: [EmailTemplate!]! """ Returns one of the company's email templates by id. Requires emails.CREATE permission. """ emailTemplate(id: SafeInt!): EmailTemplate! """ Returns grouped and aggregated reporting stats for estimates. Requires estimates.READ. """ estimateReport(where: EstimateFilter, orderBy: [EstimateReportOrderBy!], take: SafeInt, skip: SafeInt): [EstimateReportRow!]! """ Returns all estimates for the company. Defaults to non-archived, non-deleted estimates. Pass where: { isArchived: true } or { isDeleted: true } to override. Requires estimates.READ. """ estimates(where: EstimateFilter = {isArchived: false, isDeleted: false}, skip: SafeInt, take: SafeInt = 1000, orderBy: [EstimateSort!]): [Estimate!]! """ Returns a single estimate by ID, including soft-deleted records. Throws NotFoundException if not found. Requires estimates.READ. """ estimate(estimateId: SafeInt!): Estimate! """ Returns estimate line items, optionally filtered. Only line items belonging to non-archived, non-deleted estimates are returned. Requires estimates.READ. """ estimateLineItems(where: EstimateLineItemFilter, skip: SafeInt, take: SafeInt = 1000, orderBy: [EstimateLineItemSort!]): [EstimateLineItem!]! """ List event categories for the company. Excludes soft-deleted categories by default — pass where: { isDeleted: true } to include them. """ eventCategories(where: EventCategoryFilter, orderBy: [EventCategorySort!], take: SafeInt = 1000, skip: SafeInt): [EventCategory!]! """ Returns a single event category by ID. Returns null if not found or deleted. """ eventCategory(eventCategoryId: SafeInt!): EventCategory """ Accessible to all authenticated company users. Returns all forms attached to a specific event. """ eventForms(eventId: SafeInt!): [EventForm!]! """ Accessible to all authenticated company users. Returns a specific event form submission by ID. """ eventForm(eventFormId: SafeInt!): EventForm """ Returns events. Soft-deleted events are always excluded regardless of filter. If no filter is provided, a default date range of +/- 7 days UTC is applied. Users without canAccessAllEvents see only their own assigned events; users with canAccessAllEvents see all events. To include deleted events, pass isDeleted: true in the filter. """ events( """ If no where filter is provided, a default event date range of +/- 7 days UTC will be applied """ where: EventFilter orderBy: [EventSort!] take: SafeInt = 1000 skip: SafeInt ): [Event!]! """ Returns a single event by ID. Returns null if not found or soft-deleted. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). """ event(eventId: SafeInt!): Event """ Returns events for a specific user. Soft-deleted events are always excluded. If no where filter is provided, a default date range of +/- 7 days UTC is applied. Users without canAccessAllEvents are restricted to their own events regardless of the userId argument. """ eventsByUserId(userId: SafeInt!, where: EventFilter, orderBy: [EventSort!], take: SafeInt = 1000, skip: SafeInt): [Event!]! """Deprecated. Use eventsByUserId instead.""" eventsByEmployeeUserId(userId: SafeInt!, where: EventWhere @deprecated, filter: EventFilter, orderBy: [EventSort!], take: SafeInt = 1000, skip: SafeInt): [Event!]! @deprecated(reason: "Use eventsByUserId") """Returns grouped and aggregated reporting stats for events.""" eventReport(where: EventFilter, orderBy: [EventReportOrderBy!], take: SafeInt, skip: SafeInt): [EventReportRow!]! """ requires calendar.READ or higher. Returns all visible off-seasons defined for the company. """ offSeasons(where: OffSeasonFilter, orderBy: [OffSeasonSort!], take: Int, skip: Int): [OffSeason!]! """ requires calendar.READ or higher. Returns all recurring event series for the company. """ recurringEvents(where: RecurringEventFilter, orderBy: [RecurringEventSort!], take: Int, skip: Int): [RecurringEvent!]! """ requires calendar.READ or higher. Returns a single recurring event series by ID. """ recurringEvent(id: SafeInt!): RecurringEvent """ Returns grouped and aggregated reporting stats for invoices. Defaults to active, non-archived invoices. Override with e.g. `where: { deletedState: { equals: DELETED } }` to include soft-deleted invoices, or `where: { isArchived: true }` for archived ones. Requires invoices.READ. """ invoiceReport(where: InvoiceFilter, orderBy: [InvoiceReportOrderBy!], take: SafeInt, skip: SafeInt): [InvoiceReportRow!]! """ `take` defaults to 1000 but is not a hard cap — pass a higher value for larger result sets. Returns all invoices for the company. Defaults to non-archived, active invoices. Pass `where: { isArchived: true }` to include archived invoices, or `where: { deletedState: { equals: DELETED } }` to include soft-deleted invoices. Requires invoices.READ. """ invoices(where: InvoiceFilter = {isArchived: false, deletedState: {equals: ACTIVE}}, take: SafeInt = 1000, skip: SafeInt, orderBy: [InvoiceSort!]): [Invoice!]! """ Returns a single invoice by ID, including soft-deleted records. Throws NotFoundException if not found. Requires invoices.READ. """ invoice(invoiceId: SafeInt!): Invoice! """ Returns all items for the company. Defaults to non-deleted items. Pass where: { isDeleted: true } to include deleted items. Requires items.READ. """ items(where: ItemFilter = {isDeleted: false}, orderBy: [ItemSort!], take: SafeInt = 1000, skip: SafeInt): [Item!]! """ Returns all item packages for the company. Defaults to non-deleted packages. Pass where: { isDeleted: true } to include deleted packages. Requires items.READ. """ itemPackages(where: ItemPackageFilter = {isDeleted: false}, orderBy: [ItemPackageSort!], take: SafeInt = 1000, skip: SafeInt): [ItemPackage!]! """ Returns a single item by ID, including soft-deleted records. Throws NotFoundException if not found. Requires items.READ. """ item(itemId: SafeInt!): Item! """ Returns a single item package by ID, including soft-deleted records. Throws NotFoundException if not found. Requires items.READ. """ itemPackage(itemPackageId: SafeInt!): ItemPackage! """ Accessible to all authenticated company users. Returns all orders for the company. """ orders(where: OrderFilter, orderBy: [OrderSort!] = [{date: desc}, {id: desc}], take: SafeInt = 1000, skip: SafeInt): [Order!]! """ Returns a single order by ID. Throws NotFoundException if not found. Accessible to all authenticated company users. """ order(orderId: SafeInt!): Order! """ Returns grouped and aggregated reporting stats for payments. Requires payments.READ. """ paymentReport(where: PaymentFilter, orderBy: [PaymentReportOrderBy!], take: SafeInt, skip: SafeInt): [PaymentReportRow!]! """ Returns a paginated list of payments for the company. Requires payments.READ. """ payments(where: PaymentFilter, orderBy: [PaymentSort!], take: SafeInt, skip: SafeInt): [Payment!]! """ Returns a single payment by ID, including soft-deleted records. Throws NotFoundException if not found. Requires payments.READ. """ payment(paymentId: SafeInt!): Payment! """ Publicly accessible (no authentication required). Returns all available subscription plans with pricing and feature tiers. """ plans: [Plan!]! """Returns all properties for the company. Requires properties.READ.""" properties(where: PropertyFilter, orderBy: [PropertySort!], take: SafeInt = 1000, skip: SafeInt): [Property!]! """ Returns a single property by ID, including soft-deleted records. Throws NotFoundException if not found. Requires properties.READ. """ property(id: SafeInt!): Property! """ Returns all named Quick Dispatch Templates for the company, ordered by name by default. Excludes pending-visit entries. Requires dispatchBoard.READ. """ quickDispatchTemplates(where: QuickDispatchTemplateFilter, orderBy: [QuickDispatchTemplateSort!], take: SafeInt, skip: SafeInt): [QuickDispatchTemplate!]! """ Returns a single Quick Dispatch Template by ID with its full configuration. Requires dispatchBoard.READ. """ quickDispatchTemplate(id: SafeInt!): QuickDispatchTemplate! """ Aggregate route data grouped by the dimensions you select (crewId, equipmentId, date). If no dimension fields are selected, returns a single row with company-wide totals. Useful for windshield time summaries, gas spend by vehicle, and stops-per-crew reporting. Requires calendar.UPDATE. """ routeReport(where: RouteFilter, orderBy: [RouteReportOrderBy!], take: SafeInt, skip: SafeInt): [RouteReportRow!]! """ Returns future recurring event instances that already have a different assignment (crew or employees) than the one proposed. Used to surface the conflict-check dialog before propagating an assignment across a route series. Only events that belong to a recurring series are checked; one-off events in eventIds are silently skipped. Stepped by repeatEveryWeeks up to week 52. Requires calendar.UPDATE. """ upcomingAssignmentConflicts( """ Event IDs on the current route. Non-recurring events are silently skipped. """ eventIds: [SafeInt!]! """ Number of weeks between route recurrences — matches repeatEveryWeeks on the route mutations. """ repeatEveryWeeks: Int! ): [Event!]! """ Calculates route metrics for a proposed route without saving anything. Returns the same values that would be written if createRoute were called with the same inputs. Useful for previewing ETAs, drive times, and cost before committing. """ routePreview( input: CreateRouteInput! """ If true, the optimizer reorders stops before calculating. Stops are returned in optimized order. """ optimize: Boolean """ Routing provider for this request. Defaults to the configured provider. """ provider: RoutingProvider ): RoutePreview! """Returns a single route by ID. Requires calendar.UPDATE.""" route(routeId: SafeInt!): Route! """Returns routes for the company. Requires calendar.UPDATE.""" routes(where: RouteFilter, orderBy: [RouteSort!], take: SafeInt = 1000, skip: SafeInt): [Route!]! """ Get drive time and distance for an ordered list of waypoints. Uses traffic-aware routing when departAt is provided. Results are cached for 1 week. """ directions( waypoints: [CoordinateInput!]! """ISO 8601 departure datetime. Omit to use non-traffic routing.""" departAt: DateTime """Cost per gallon for fuel cost estimate.""" costPerGallon: Float """Miles per gallon for fuel cost estimate.""" milesPerGallon: Float ): DirectionsResult! @deprecated(reason: "Use routePreview for event-based route previews, or Route.directions for a saved route.") """ Returns the given events in optimized order plus route metrics. Read-only — call `createRoute` with the returned event order (and optional `pinnedFirstEventId`) to persist it. Every requested event must belong to the company; an unknown id is an error rather than a silently smaller route. Errors too if the optimizer cannot fit a stop, naming the stop and the reason. Distances and times come from a directions call, not from the optimizer, so the legs always sum to the total. Cached for a week per provider, keyed by the stops and the day and hour of departure. """ optimizedRoute( """ Event IDs to optimize. Input order does not matter unless `pinnedFirstEventId` is set — then that id must appear in the list and is fixed as stop 1. """ eventIds: [SafeInt!]! """ ISO 8601 departure datetime. Defaults to earliest event start or 8am local. """ departAt: DateTime """ Optional depot start location. Required when startLocationType is CUSTOM. """ startLocation: CoordinateInput """Optional depot end location. Required when endLocationType is CUSTOM.""" endLocation: CoordinateInput """ How the start depot is chosen. Omit for COMPANY. Pass NONE for an open start (optimizer may choose the first stop). """ startLocationType: RouteEndpointType """ How the end depot is chosen. Omit for COMPANY. Pass NONE for an open end (no return-to-depot leg). """ endLocationType: RouteEndpointType """ Employee whose on-file address is the start. Required when startLocationType is USER. """ startUserId: SafeInt """ Employee whose on-file address is the end. Required when endLocationType is USER. """ endUserId: SafeInt """ Crew whose depot is used when startLocationType or endLocationType is CREW. """ crewId: SafeInt """ Routing provider for this request. Defaults to the configured provider. """ provider: RoutingProvider """ Event id to keep as stop 1. Must be one of `eventIds`. The remaining stops are optimized after it. When a start depot is present, returned metrics include the depot → pinned leg; with an open start they begin at the pin. Prefer `routePreview(input: { pinnedFirstEventId }, optimize: true)`. """ pinnedFirstEventId: SafeInt ): OptimizeRouteResult! @deprecated(reason: "Use routePreview(input: ..., optimize: true) instead. routePreview returns per-stop ETAs and supports the full CreateRouteInput shape.") """ Returns grouped and aggregated reporting stats for SMS messages. Requires texts.READ. """ smsReport(where: SmsFilter, orderBy: [SmsReportOrderBy!], take: SafeInt, skip: SafeInt): [SmsReportRow!]! """Returns SMS messages for the company. Requires texts.READ permission.""" smsMessages(where: SmsFilter, orderBy: [SmsSort!], take: SafeInt = 100, skip: SafeInt): [Sms!]! """ Returns the company's SMS templates, ordered by subject. Requires texts.CREATE permission. """ smsTemplates: [SmsTemplate!]! """ Returns one of the company's SMS templates by id. Requires texts.CREATE permission. """ smsTemplate(id: SafeInt!): SmsTemplate! """ Validates a promo code for the given plan and previews the resulting checkout total so the UI can show the discounted price before creating the subscription. Returns isValid:false (not an error) for blank, unknown, or inactive codes. Requires role.OWNER. """ validatePromoCode(tier: SubscriptionTier!, billingInterval: BillingInterval!, promoCode: String!): PromoCodeValidation! tags(where: TagFilter, orderBy: [TagOrderBy!], take: Int, skip: Int): [Tag!]! """ Accessible to users with timeTracking: READ_OWN or higher. Users without READ_ALL are scoped to their own entries. """ timeEntryReport(where: TimeEntryFilter, orderBy: [TimeEntryReportOrderBy!], take: SafeInt, skip: SafeInt): [TimeEntryReportRow!]! timeEntryCategories(orderBy: [TimeEntryCategorySort]): [TimeEntryCategory!]! @deprecated timeEntryCategoryByName(name: String!): TimeEntryCategory @deprecated """ Accessible to all users. By default results are scoped to the current user (timeTracking: READ_OWN). Users with READ_ALL or higher can query any user. """ timeEntriesByEmployeeUserId( userId: SafeInt! take: SafeInt = 1000 skip: SafeInt """ If no where filter is provided, a default event date range of +/- 7 days UTC will be applied """ where: TimeEntryFilter orderBy: [TimeEntrySort!] ): [TimeEntry!]! """ Accessible to all users. Throws ForbiddenException for entries belonging to other users unless the caller has timeTracking: READ_ALL or higher. Throws NotFoundException if no entry exists with the given ID. """ timeEntry(timeEntryId: SafeInt!): TimeEntry! """ Returns true if the current user is/was clocked in. A user is considered clocked in if they have an active timer or if they have a clock in event today that has not yet been followed by a clock out event. """ isClockedIn(at: DateTime): Boolean! """Return the currently running timer for the current user, if it exists.""" runningTimer(where: TimeEntryFilter): TimeEntry """ Return the time entry for the current user that is/was active at the given time, inclusive of running timers. """ activeTimeEntry( where: TimeEntryFilter """Timestamp that the timer was running, if in the past""" at: DateTime ): TimeEntry """ Accessible to all users. By default results are scoped to the current user's entries (timeTracking: READ_OWN). Users with READ_ALL or higher can see all entries. """ timeEntries(where: TimeEntryFilter, orderBy: [TimeEntrySort!], take: SafeInt = 100, skip: SafeInt): [TimeEntry!]! twilioVerifications: [TwilioVerification!] """ Accessible to all authenticated company users. Returns all upsells configured for the company. """ upsells(where: UpsellFilter, orderBy: [UpsellSort!], take: SafeInt, skip: SafeInt): [Upsell!]! """ Returns a single upsell by ID. Throws NotFoundException if not found. Accessible to all authenticated company users. """ upsell(upsellId: SafeInt!): Upsell! """Returns all day overrides for the company. Requires users.READ.""" userDayOverrides(where: UserDayOverrideFilter, orderBy: [UserDayOverrideSort!], take: SafeInt = 1000, skip: SafeInt): [UserDayOverride!]! """ Returns all employees for the company. Defaults to active (non-deleted) users. Pass where: { isDeleted: true } to include deleted users. Requires users.READ. """ employees(where: UserFilter = {isDeleted: false}, orderBy: [UserSort!], take: SafeInt = 1000, skip: SafeInt): [User] @deprecated(reason: "Use 'users'") """ Returns a user by ID, or the currently authenticated user if no ID is provided. Examples: - Current user: user { id firstName lastName role } - Lookup by ID: user(userId: 42) { id firstName lastName } """ user(userId: SafeInt): User """Returns the currently authenticated user's profile.""" currentUser: User """ Returns a single user by ID regardless of deleted status. Throws NotFoundException if not found. """ userById(userId: SafeInt!): User @deprecated(reason: "Use user(userId:) for lookup by ID, or currentUser for the authenticated user") """ Returns all users for the company. Defaults to active (non-deleted) users. Pass where: { isDeleted: true } to include deleted users. Requires users.READ. """ users(where: UserFilter = {isDeleted: false}, orderBy: [UserSort!], take: SafeInt = 1000, skip: SafeInt): [User!]! """ Returns all vendors for the company. Defaults to non-deleted vendors. Pass where: { isDeleted: true } to include deleted vendors. Requires vendors.READ. """ vendors(where: VendorFilter = {isDeleted: false}, orderBy: [VendorSort!], take: SafeInt = 1000, skip: SafeInt): [Vendor!]! """ Returns a single vendor by ID, including soft-deleted records. Throws NotFoundException if not found. Requires vendors.READ. """ vendor(vendorId: SafeInt!): Vendor! """ List wiki pages for the company. Defaults to isActive: true — pass where.isActive to override. """ wikis(where: WikiFilter, orderBy: [WikiSort!], take: SafeInt = 1000, skip: SafeInt): [Wiki!]! """ Returns a wiki page by ID (including inactive). Throws if not found or outside the company. """ wiki(wikiId: SafeInt!): Wiki! """ Look up tax rates for a US address. Returns component rates (county, city, state) and the combined total, all expressed as percentages (e.g. 8.25 for 8.25%). Requires PROPERTIES:READ permission. For the most accurate result supply the full street address along with city and stateCode — ZipTax can then resolve to the exact tax jurisdiction rather than relying solely on the ZIP code. """ lookupTaxRate( """ US ZIP code (5-digit or ZIP+4). Optional when city/state or full address can identify the jurisdiction. """ zip: String """ Existing property ID. Enables Redis caching only when the property belongs to the current company and matches the lookup address. """ propertyId: Int """ City name — improves accuracy when multiple jurisdictions share the ZIP """ city: String """County name — used as a secondary filter when multiple results match""" county: String """ Street address — enables ZipTax address-level lookup for maximum accuracy """ address: String """Two-letter US state code (e.g. TX)""" stateCode: String ): TaxRateLookupResult! } type Mutation { """ Registers a new company and user account. Public endpoint — no authentication required. """ signUp(input: SignUpInput!): SignUpResult! """ Updates contact information for the company and user. Restricted to ADMIN and OWNER roles. """ updateContactInfo(companyContact: CompanyContactInput!, userContact: UserContactInput!): Company! """Health check mutation. Returns 'pong' to confirm the API is reachable.""" pingMutation: String! """ Generates a pre-signed S3 upload URL for a temporary file. Returns the upload URL and file key. """ createUpload(contentType: String!, size: SafeInt!): TempFile! """ Send a message to the AI assistant and receive a response. Available on Enterprise plans only. Not available to MCP or OAuth API callers — direct browser/app sessions only. Omit `chatId` to start a new conversation. Pass the `id` from a prior `AiChat` response to continue an existing thread. `responseFormat` defaults to MARKDOWN. Pass PLAIN_TEXT for SMS/email contexts. `contextData` is an optional opaque JSON blob passed verbatim into the AI system prompt. Use it to supply page context (e.g. currently-viewed record IDs, date range, feature area) without schema churn. """ sendAiMessage(message: String!, chatId: SafeInt, responseFormat: AiResponseFormat, contextData: JSON): AiChat! """ Create a new area, appended to the end of the list. Company cap is 100 live areas. """ createArea(input: CreateAreaInput!): Area! """Update an existing area. Fields left unset or null are unchanged.""" updateArea(id: SafeInt!, input: UpdateAreaInput!): Area! """Soft-delete an area.""" deleteArea(id: SafeInt!): Boolean! """ Set the order of the company's areas in one atomic write. areaIds must list every live area exactly once, in the new order: duplicates or a partial list are rejected (400) and an id that is not a live area of this company is 404. Positions are written 1-based: the first id gets sortOrder 1, the last gets the number of areas. Returns all live areas in the new order. """ reorderAreas(areaIds: [SafeInt!]!): [Area!]! """Creates a new crew for the company. Requires crews.CREATE.""" createCrew(input: CreateCrewInput!): Crew! """Updates an existing crew. Requires crews.UPDATE.""" updateCrew(crewId: SafeInt!, input: UpdateCrewInput!): Crew! """ Soft-deletes a crew (isDeleted = true). Open events retain the crew reference. Requires crews.DELETE. """ deleteCrew(crewId: SafeInt!): Boolean! """ Restores a soft-deleted crew (isDeleted = false). Requires crews.UPDATE. """ restoreCrew(crewId: SafeInt!): Crew! """ Replaces all members of a crew with the provided user list. Optionally reassigns open/waitlisted events to the new member list. Requires crews.UPDATE. """ setCrewMembers(crewId: SafeInt!, userIds: [SafeInt!]!, reassignOpenEvents: Boolean = false): Crew! """ Adds a single member to a crew. Optionally reassigns open/waitlisted events to the updated member list. Requires crews.UPDATE. """ addCrewMember(crewId: SafeInt!, userId: SafeInt!, reassignOpenEvents: Boolean = false): Crew! """ Removes a single member from a crew. Optionally reassigns open/waitlisted events to the updated member list. Requires crews.UPDATE. """ removeCrewMember(crewId: SafeInt!, userId: SafeInt!, reassignOpenEvents: Boolean = false): Crew! """Create a new customer for the company. Requires customers.CREATE.""" createCustomer(input: CustomerInput!): Customer! """Update an existing customer. Requires customers.UPDATE.""" updateCustomer(customerId: SafeInt!, input: CustomerInput!): Customer! """ Set the active or inactive status of a customer. WRITTEN_OFF and PENDING_APPROVAL are read-only states and will be rejected. Requires customers.UPDATE. """ setCustomerStatus(customerId: SafeInt!, status: CustomerStatus!): Customer! """ Add or subtract from a customer's credit balance. Positive amount adds credit, negative subtracts. The resulting balance cannot go below 0. Requires customers.UPDATE. """ adjustCustomerCredit(customerId: SafeInt!, amount: Decimal!, note: String): Customer! """ Mints a short-lived token for the Beefree email editor and returns it verbatim. A mutation, not a query: it exchanges the company's Beefree credentials for a session-scoped token, so it must not be cached or prefetched. The payload is passed straight to the editor SDK (which consumes the whole token object, not just the access token), so it stays untyped rather than narrowing fields the vendor may add. Requires emails.READ. """ createEmailEditorToken: JSON! """Creates a new estimate. Requires estimates.CREATE.""" createEstimate(input: CreateEstimateInput): Estimate! """Updates an existing estimate. Requires estimates.UPDATE.""" updateEstimate(estimateId: SafeInt!, input: UpdateEstimateInput): Estimate! """Deletes an estimate. Requires estimates.DELETE.""" deleteEstimate(estimateId: SafeInt!): Estimate! """Accepts an estimate. Requires estimates.UPDATE.""" acceptEstimate(estimateId: SafeInt!): Estimate! """Sets an estimate's status to Draft. Requires estimates.UPDATE.""" setEstimateStatusToDraft(estimateId: SafeInt!): Estimate! """ Sets an estimate's status to Invoiced without creating an invoice record. Requires estimates.UPDATE. """ setEstimateStatusToInvoiced(estimateId: SafeInt!): Estimate! """ Duplicates an estimate with a new number, today's date, and draft status. Requires estimates.CREATE. """ copyEstimate(estimateId: SafeInt!): Estimate! """Restores a soft-deleted estimate. Requires estimates.UPDATE.""" restoreEstimate(estimateId: SafeInt!): Estimate! """Marks an estimate as sent via copied link. Requires estimates.UPDATE.""" markEstimateAsCopyLinkSent(estimateId: SafeInt!): Estimate! """ Creates an invoice from an estimate and marks the estimate as invoiced. Requires estimates.UPDATE. """ createInvoiceFromEstimate(estimateId: SafeInt!): Invoice! """ Toggles optional line item inclusion on an estimate. Requires estimates.UPDATE. """ toggleEstimateLineItem(estimateId: SafeInt!, lineItemId: SafeInt!, include: Boolean!): Estimate! """Sets an estimate's status to Pending. Requires estimates.UPDATE.""" setEstimateStatusToPending(estimateId: SafeInt!): Estimate! """ Sets an estimate's status to Changes Requested. Requires estimates.UPDATE. """ setEstimateStatusToChangesRequested(estimateId: SafeInt!, requestedChanges: String!): Estimate! """ Sets an estimate's status to Changes Updated. Requires estimates.UPDATE. """ setEstimateStatusToChangesUpdated(estimateId: SafeInt!): Estimate! """ Declines an estimate with a reason and optional comment. Requires estimates.UPDATE. """ declineEstimate(estimateId: SafeInt!, reason: EstimateActionReason!, comment: String): Estimate! """Accepts multiple estimates. Requires estimates.UPDATE.""" acceptEstimates(estimateIds: [SafeInt!]!): [Estimate!]! """Archives multiple estimates. Requires estimates.UPDATE.""" archiveEstimates(estimateIds: [SafeInt!]!): [Estimate!]! """Unarchives multiple estimates. Requires estimates.UPDATE.""" unarchiveEstimates(estimateIds: [SafeInt!]!): [Estimate!]! """Deletes multiple estimates. Requires estimates.DELETE.""" deleteEstimates(estimateIds: [SafeInt!]!): [Estimate!]! """Creates a line item on an estimate. Requires estimates.UPDATE.""" createEstimateLineItem(estimateId: SafeInt!, input: EstimateLineItemInput): Estimate! """Updates a line item on an estimate. Requires estimates.UPDATE.""" updateEstimateLineItem(estimateId: SafeInt!, lineItemId: SafeInt!, input: EstimateLineItemInput): Estimate! """ Sets the event status of an estimate line item. Requires estimates.UPDATE. """ setEstimateLineItemEventStatus(estimateId: SafeInt!, lineItemId: SafeInt!, eventStatus: EstimateLineItemEventStatus!, eventId: SafeInt): EstimateLineItem! """Deletes a line item from an estimate. Requires estimates.DELETE.""" deleteEstimateLineItem(estimateId: SafeInt!, lineItemId: SafeInt!): Estimate! """ Generate a PDF representing selected estimates as a table/list (list-as-shown). Returns URL to the generated PDF. Requires estimates.UPDATE. """ createEstimateListPdf(estimateIds: [SafeInt!]!): String! """ Generate a page-per-estimate PDF for selected estimates. Returns URL to the generated PDF. Requires estimates.UPDATE. """ createEstimatesPdf(estimateIds: [SafeInt!]!, style: EstimatePrintStyle = CLEAN): String! """ Send estimates via EMAIL, MAIL, or SMS. Returns per-estimate status (QUEUED, SENT, or ERROR) with actionable error details. Requires estimates.UPDATE. """ sendEstimates(estimateIds: [SafeInt!]!, sendMethod: EstimateSendMethod!): EstimateSendResults! """ Manually mark estimates as sent (or clear sent flags for NOT_SENT) without triggering actual send. Requires estimates.UPDATE. """ markEstimatesAsSent(estimateIds: [SafeInt!]!, sentByType: EstimateSentByType!): [Estimate!]! """ Decline estimates with the same reason and optional comment for all. Requires estimates.UPDATE. """ declineEstimates(estimateIds: [SafeInt!]!, reason: EstimateActionReason!, comment: String): [Estimate!]! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event) """ updateBeforeDescription(beforeAndAfterId: SafeInt!, description: String!): BeforeAndAfter! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event) """ updateAfterDescription(beforeAndAfterId: SafeInt!, description: String!): BeforeAndAfter! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event) """ addBeforeMedia(beforeAndAfterId: SafeInt!, tempFileName: String!): BeforeAndAfter! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event) """ addAfterMedia(beforeAndAfterId: SafeInt!, tempFileName: String!): BeforeAndAfter! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event) """ removeAfterMedia(beforeAndAfterId: SafeInt!, mediaId: SafeInt!): BeforeAndAfter! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event) """ removeBeforeMedia(beforeAndAfterId: SafeInt!, mediaId: SafeInt!): BeforeAndAfter! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event) """ removeBeforeAndAfter(beforeAndAfterId: SafeInt!): BeforeAndAfter! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event) """ addBeforeAndAfter(eventId: SafeInt!): BeforeAndAfter! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event) """ updateIsClientVisible(beforeAndAfterId: SafeInt!, isClientVisible: Boolean!): BeforeAndAfter! """ Update form content and signature (handles both initial submission and updates). Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event). """ updateEventForm(eventFormId: SafeInt!, content: String!, signature: String): EventForm! """ Creates a single (non-recurring) event. Requires calendar.CREATE. """ createEvent(input: CreateEventInput!): Event! """ Attaches an existing form to an event. Requires calendar.UPDATE. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). """ addEventForm(eventId: SafeInt!, formId: SafeInt!): Event! """ Removes a form attachment from an event. Requires calendar.UPDATE. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). """ removeEventForm(eventId: SafeInt!, formId: SafeInt!): Event! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event). Requires calendar.UPDATE. """ updateEventDescription(eventId: SafeInt!, description: String!): Event! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event). Requires calendar.UPDATE, requires calendar.COMPLETE_AND_INVOICE (for CLOSED status), or requires calendar.SKIP (for SKIPPED status). """ updateEventStatus(eventId: SafeInt!, status: EventStatus!, at: DateTime): Event! """ Accessible to all calendar users; users without requires calendar.ACCESS_ALL are scoped to their own assigned events. """ updateEventStatuses(eventIds: [SafeInt!]!, status: EventStatus!, at: DateTime): [Event!]! """ Marks an event as closed/complete. Requires calendar.COMPLETE_AND_INVOICE. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). Optionally pass invoiceDateSource to control how invoice line item dates are stamped. """ closeEvent(eventId: SafeInt!, at: DateTime, invoiceDateSource: EventInvoiceDateSource): Event! """ Marks multiple events as closed/complete. Requires calendar.COMPLETE_AND_INVOICE. Accessible to users assigned to the events (or requires calendar.ACCESS_ALL). Optionally pass invoiceDateSource to control how invoice line item dates are stamped. """ closeEvents(eventIds: [SafeInt!]!, at: DateTime, invoiceDateSource: EventInvoiceDateSource): [Event!]! """ Reopens one or more closed, skipped, or cancelled events, returning them to OPEN status. Clears closedAt, skippedAt, skippedReason, completedByUserId, and invoiceId. Only CLOSED, SKIPPED, and CANCELLED events are affected — OPEN and WAITLISTED events are silently bypassed. Requires calendar.COMPLETE_AND_INVOICE. Accessible to users assigned to the events (or requires calendar.ACCESS_ALL). """ reopenEvents(eventIds: [SafeInt!]!): [Event!]! """ Reopens a single closed, skipped, or cancelled event. See reopenEvents for full semantics. """ reopenEvent(eventId: SafeInt!): Event! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event). Requires calendar.SKIP. """ skipEvent(eventId: SafeInt!, skippedReason: String!, at: DateTime): Event! """ Marks multiple events as skipped. Requires calendar.SKIP. Accessible to users assigned to the events (or requires calendar.ACCESS_ALL). """ skipEvents(eventIds: [SafeInt!]!, skippedReason: String!, at: DateTime): [Event!]! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event). """ createEventDispatchNote(eventId: SafeInt!, input: DispatchNoteInput!): Event! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event). Requires calendar.SKIP. """ updateEventSkippedReason(eventId: SafeInt!, skippedReason: String!, at: DateTime): Event! """ Bulk-schedules multiple events to the same start date. Same rules as scheduleEvent — each event moves to OPEN if waitlisted, throws if closed/skipped/cancelled. Existing end dates are shifted by the same interval to preserve multi-day duration. Each event retains its own budgetedHours and startTime. Accessible to users assigned to each event (or requires calendar.ACCESS_ALL). Requires calendar.UPDATE. """ scheduleEvents( eventIds: [SafeInt!]! """The new start date for all events, in the company's timezone""" startDate: LocalDate! ): [Event!]! """ Schedules an event. If the event is waitlisted, it will move to OPEN. If the event is closed, skipped, or cancelled, an error will be thrown. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event). Requires calendar.UPDATE. """ scheduleEvent( eventId: SafeInt! """The date/start date of the event in the company's timezone""" startDate: LocalDate! """Optional start time of the event in the company's timezone""" startTime: LocalTime """ Exact inclusive last active date for a multi-day event. Can only be provided if startTime is null. """ endDate: LocalDate """ Budgeted hours / duration of the event. This is used to calculate endTime if startTime is provided. If not provided, the existing value will be used. """ budgetedHours: Decimal ): Event! """ Rename an event. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). Requires calendar.UPDATE. """ updateEventTitle(eventId: SafeInt!, title: String!): Event! """ Set or clear the category on an event (e.g. todo/meeting type). Pass null to unset. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). Requires calendar.UPDATE. """ updateEventCategory(eventId: SafeInt!, categoryId: SafeInt): Event! """ Change the property an event is scheduled at. Pass null to unlink. A VISIT cannot be left without a property: use setEventLocation to move a visit to a custom location, which clears the property in the same call. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). Requires calendar.UPDATE. """ updateEventProperty(eventId: SafeInt!, propertyId: SafeInt): Event! @deprecated(reason: "Use setEventLocation instead.") """ Reassign an event to a different customer. Clears propertyId unless a new one is explicitly provided, except that a VISIT with a property must be given a new propertyId (or moved to a custom location with setEventLocation first). A VISIT at a custom location keeps it. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). Requires calendar.UPDATE. """ updateEventCustomer(eventId: SafeInt!, customerId: SafeInt!, propertyId: SafeInt): Event! """ Link or unlink a contract on an event. Pass null contractId to unlink. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). Requires calendar.UPDATE. """ updateEventContract(eventId: SafeInt!, contractId: SafeInt): Event! """ Set the color of an event. Pass an empty string to clear. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). Requires calendar.UPDATE. """ updateEventColor(eventId: SafeInt!, color: String!): Event! """ Set whether a visit generates an invoice. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). Requires calendar.UPDATE. """ updateEventInvoiceable(eventId: SafeInt!, isInvoiceable: Boolean!): Event! """ Upsert custom field values on an event. Each fieldId must be a DynamicCustomField that belongs to this company and applies to events. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). Requires calendar.UPDATE. """ updateEventCustomFields(eventId: SafeInt!, customFields: [CustomFieldInput!]!): Event! """ Add a line item to an event. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). Requires calendar.UPDATE. """ createEventLineItem(eventId: SafeInt!, input: EventLineItemInput!): Event! """ Update a line item on an event. All fields are optional — omitted fields are left unchanged. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). Requires calendar.UPDATE. """ updateEventLineItem(eventId: SafeInt!, lineItemId: SafeInt!, input: UpdateEventLineItemInput!): Event! """ Remove a line item from an event. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). Requires calendar.UPDATE. """ deleteEventLineItem(eventId: SafeInt!, lineItemId: SafeInt!): Event! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event). Add media (photo or video) to an event. """ addEventMedia(eventId: SafeInt!, tempFileName: String!): Event! """ Accessible to users assigned to the event (or requires calendar.ACCESS_ALL to access any event). Remove media from an event. """ removeEventMedia(eventId: SafeInt!, mediaId: SafeInt!): Event! """ Set where an event happens. This is a total replacement of the event's location, not a partial update: - propertyId only: the location is derived from the property and any custom location is discarded. - location only: the custom location is written as given (no geocoding) and propertyId is cleared. - neither: both the property and the custom location are cleared. - both: rejected, they are mutually exclusive. A VISIT requires exactly one of the two. Other event types may have neither. Custom locations must carry coordinates — the route board, dispatch and maps all gate on them. Accessible to users assigned to the event (or requires calendar.ACCESS_ALL). Requires calendar.UPDATE. """ setEventLocation(eventId: SafeInt!, propertyId: SafeInt, location: LocationInput): Event! """Hard-delete events by IDs. Requires calendar.DELETE.""" deleteEvents(eventIds: [SafeInt!]!): [SafeInt!]! """ Creates standalone copies of one or more events. Copies core fields, line items, user assignments, location, tags, visit photos, and visit forms. The recurringEventId is never copied — copied events are always standalone. Requires calendar.CREATE. """ copyEvents(eventIds: [SafeInt!]!): [Event!]! """ Assigns a crew to multiple events. Sets crewId AND replaces event_employees with the crew's current members. Pass crewId: null to clear both. Requires calendar.UPDATE. """ assignEventsCrew(eventIds: [SafeInt!]!, crewId: SafeInt): [Event!]! """ Assigns a crew to a single event. Sets crewId AND replaces event_employees with the crew's current members. Pass crewId: null to clear both. Requires calendar.UPDATE. """ assignEventCrew(eventId: SafeInt!, crewId: SafeInt): Event! """ Replaces employee assignments on multiple events. Clears crewId since the event is now individually staffed. userIds: [] removes all. Requires calendar.UPDATE. """ assignEventsUsers(eventIds: [SafeInt!]!, userIds: [SafeInt!]!): [Event!]! """ Replaces employee assignments on a single event. Clears crewId since the event is now individually staffed. userIds: [] removes all. Requires calendar.UPDATE. """ assignEventUsers(eventId: SafeInt!, userIds: [SafeInt!]!): Event! addEventTag(eventId: SafeInt!, tag: String!): Event! removeEventTag(eventId: SafeInt!, tag: String!): Event! addRecurringEventTag(recurringEventId: SafeInt!, tag: String!): RecurringEvent! removeRecurringEventTag(recurringEventId: SafeInt!, tag: String!): RecurringEvent! """ Generates a multi-event PDF for the given events and returns the PHP download URL. The caller opens the URL directly in their browser (shared http-only auth cookie). Requires calendar.ACCESS_ALL, or the user must be assigned to all requested events. """ createEventsPdf( eventIds: [SafeInt!]! """ When true, line item prices are included in the PDF. Defaults to false. """ showPrices: Boolean ): String! """ Queues one email per selected event. Provide exactly one of messageBody or templateId. """ sendCustomerEmailForEvents(eventIds: [SafeInt!]!, subject: String!, messageBody: String, templateId: SafeInt): EventEmailSendResults! """ Texts the customer of each event. Provide exactly one of messageBody or templateId; event and customer merge tags (e.g. {{EVENT_DATE}}) are applied per event. Messages are queued and sent in the background; returns per-event status (QUEUED or ERROR). Requires texts.CREATE. """ sendCustomerSmsForEvents(eventIds: [SafeInt!]!, messageBody: String, templateId: SafeInt): EventSmsSendResults! """ Creates a new invoice for the given customer. Invoice number, totals, global line items, and terms/notes are all resolved server-side. Requires invoices.CREATE. """ createInvoice(input: CreateInvoiceInput!): Invoice! """ Promote a temp S3 upload and attach it to the invoice. Requires invoices.UPDATE. """ addInvoiceMedia(invoiceId: SafeInt!, tempFileName: String!): Invoice! """ Remove media from an invoice (and delete S3 object when stored). Requires invoices.UPDATE. """ removeInvoiceMedia(invoiceId: SafeInt!, mediaId: SafeInt!): Invoice! """ Mark invoices as sent and queue delivery via EMAIL, SMS, or SNAIL_MAIL (PHP handles delivery via INVOICE_SENT). Requires invoices.UPDATE. """ sendInvoices(invoiceIds: [SafeInt!]!, sendMethod: InvoiceSendMethod!): [Invoice!]! """ Manually mark invoices as sent without triggering actual delivery. Requires invoices.UPDATE. """ markInvoicesAsSent(invoiceIds: [SafeInt!]!, sendMethod: InvoiceSendMethod!): [Invoice!]! """Write off one or more invoices. Requires invoices.UPDATE.""" writeOffInvoices(invoiceIds: [SafeInt!]!): [Invoice!]! """Archive one or more invoices. Requires invoices.UPDATE.""" archiveInvoices(invoiceIds: [SafeInt!]!): [Invoice!]! """Unarchive one or more invoices. Requires invoices.UPDATE.""" unarchiveInvoices(invoiceIds: [SafeInt!]!): [Invoice!]! """Soft-delete a single invoice. Requires invoices.DELETE.""" deleteInvoice(invoiceId: SafeInt!): Invoice! """Soft-delete multiple invoices. Requires invoices.DELETE.""" deleteInvoices(invoiceIds: [SafeInt!]!): [Invoice!]! """Restore a soft-deleted invoice. Requires invoices.UPDATE.""" restoreInvoice(invoiceId: SafeInt!): Invoice! """ Duplicate an invoice with a fresh number and draft status. Requires invoices.CREATE. """ copyInvoice(invoiceId: SafeInt!): Invoice! """Generate a PDF download URL for an invoice. Requires invoices.READ.""" createInvoicePdf(invoiceId: SafeInt!): String! """Add a line item to an invoice. Requires invoices.UPDATE.""" createInvoiceLineItem(invoiceId: SafeInt!, input: CreateInvoiceLineItemInput!): Invoice! """Update a line item on an invoice. Requires invoices.UPDATE.""" updateInvoiceLineItem(invoiceId: SafeInt!, lineItemId: SafeInt!, input: UpdateInvoiceLineItemInput!): Invoice! """ Upsert custom field values on an invoice. Each fieldId must be a DynamicCustomField that belongs to this company and applies to invoices. Requires invoices.UPDATE. """ updateInvoiceCustomFields(invoiceId: SafeInt!, customFields: [CustomFieldInput!]!): Invoice! """Delete a line item from an invoice. Requires invoices.DELETE.""" deleteInvoiceLineItem(invoiceId: SafeInt!, lineItemId: SafeInt!): Invoice! """Sets isDeleted to true for the given Items""" deleteItems(itemIds: [SafeInt!]!): Boolean! """Sets isDeleted to false for the given Items""" restoreItems(itemIds: [SafeInt!]!): Boolean! adjustItemPrices( itemIds: [SafeInt!]! """Amount to adjust the price (negative is decrease)""" amount: Decimal! """Whether amount is a percentage (vs fixed) adjustment""" isPercent: Boolean = false """Whether to also update the unit price for open EventItems""" updateOpenEventItems: Boolean = false ): Boolean! createItem(input: ItemInput!): Item! updateItem(itemId: SafeInt!, input: ItemUpdateInput!, updateOpenVisits: Boolean): Item! """ Cancels a payment, issuing a Stripe refund if applicable. Requires payments.DELETE. """ deletePayment(paymentId: SafeInt!): Payment! """Cancels multiple payments in bulk. Requires payments.DELETE.""" deletePayments(paymentIds: [SafeInt!]!): [Payment!]! """ Creates a downloadable PDF report of payments by forwarding to the PHP PDF controller. Two modes: - Current-page export: supply paymentIds (the IDs visible on the current page). - Full date-range export: supply where/orderBy matching the current search filters. Returns a URL to the generated PDF file, or null when PDF generation fails. Requires payments.READ. """ createPaymentsPdf(paymentIds: [SafeInt!], where: PaymentFilter, orderBy: [PaymentSort!]): String """Create a new property for a customer. Requires properties.CREATE.""" createProperty(input: PropertyInput!): Property! """Update an existing property. Requires properties.UPDATE.""" updateProperty(propertyId: SafeInt!, input: UpdatePropertyInput!): Property! """Add media (photo or video) to a property. Requires properties.UPDATE.""" addPropertyMedia(propertyId: SafeInt!, tempFileName: String!): Property! """Remove media from a property. Requires properties.UPDATE.""" removePropertyMedia(propertyId: SafeInt!, mediaId: SafeInt!): Property! """ Creates a Quick Dispatch Template with a company-unique name, optionally with its properties, in one transaction: if any property is invalid, nothing is created. Requires dispatchBoard.UPDATE. """ createQuickDispatchTemplate(input: CreateQuickDispatchTemplateInput!): QuickDispatchTemplate! """ Changes the template name. Preserves all properties. Requires dispatchBoard.UPDATE. """ renameQuickDispatchTemplate(id: SafeInt!, name: String!): QuickDispatchTemplate! """ Permanently deletes a template and all of its properties, line items, and employee assignments. Requires dispatchBoard.UPDATE. """ deleteQuickDispatchTemplate(id: SafeInt!): Boolean! """ Adds a property to a template. Must include at least one line item. Requires dispatchBoard.UPDATE. """ addQuickDispatchTemplateProperty(templateId: SafeInt!, input: AddQuickDispatchTemplatePropertyInput!): QuickDispatchTemplateProperty! """ Updates crew, isInvoiceable flag, note, or ordering on a property. Only supplied fields are changed. Requires dispatchBoard.UPDATE. """ updateQuickDispatchTemplateProperty(templatePropertyId: SafeInt!, input: UpdateQuickDispatchTemplatePropertyInput!): QuickDispatchTemplateProperty! """ Removes a property and all of its line items and employee assignments. Requires dispatchBoard.UPDATE. """ removeQuickDispatchTemplateProperty(templatePropertyId: SafeInt!): Boolean! """ Sets the ordering of each property by position in the supplied list (0-based). Every ID must belong to the template and appear once, or the request is a 400. Properties left out keep their current position. Requires dispatchBoard.UPDATE. """ reorderQuickDispatchTemplateProperties(templateId: SafeInt!, templatePropertyIds: [SafeInt!]!): QuickDispatchTemplate! """ Adds a line item to a template property and returns the property. Requires dispatchBoard.UPDATE. """ createQuickDispatchTemplateLineItem(templatePropertyId: SafeInt!, input: QuickDispatchTemplateLineItemInput!): QuickDispatchTemplateProperty! """ Updates a line item and returns its template property. Only supplied fields are changed. Recomputes total on price/quantity/tax changes. Requires dispatchBoard.UPDATE. """ updateQuickDispatchTemplateLineItem(templatePropertyId: SafeInt!, lineItemId: SafeInt!, input: UpdateQuickDispatchTemplateLineItemInput!): QuickDispatchTemplateProperty! """ Deletes a line item and returns its template property. Requires dispatchBoard.UPDATE. """ deleteQuickDispatchTemplateLineItem(templatePropertyId: SafeInt!, lineItemId: SafeInt!): QuickDispatchTemplateProperty! """ Replaces the full employee assignment list for a property. Pass an empty array to clear. Requires dispatchBoard.UPDATE. """ setQuickDispatchTemplatePropertyUsers(templatePropertyId: SafeInt!, userIds: [SafeInt!]!): QuickDispatchTemplateProperty! """ Dispatches a Quick Dispatch Template for a given date, creating one visit per property in the template. Returns the created visits. Requires dispatchBoard.UPDATE and calendar.CREATE. """ dispatchQuickDispatchTemplate(input: DispatchQuickDispatchTemplateInput!): [DispatchedVisit!]! """ Creates a new route for a given date and set of events. If repeatEveryWeeks is set (> 0), the route is propagated to the same day of week every N weeks up to week 52, and all created routes share a series key. Future instances are only created for events that are themselves recurring instances; a route built from one-off events gets the series key but no future routes. One-off routes (repeatEveryWeeks omitted or 0) get seriesKey: null. Stops for the given eventIds are displaced from any other routes on the same date. Routes that become empty as a result are deleted. Requires calendar.UPDATE. """ createRoute( input: CreateRouteInput! """ Number of weeks between recurrences. E.g. 2 = every 2 weeks on the same day of week, up to week 52. Omit for a one-off route. """ repeatEveryWeeks: Int """ Controls assignment propagation across future route occurrences. Only applies when repeatEveryWeeks is set. See RepeatAssignmentOption. """ repeatAssignment: RepeatAssignmentOption = OFF """ If true, the configured optimizer reorders stops before saving and eagerly populates directions fields. """ optimize: Boolean """ Routing provider for this request. Defaults to the configured provider. """ provider: RoutingProvider ): Route! """ Updates an existing route by ID. The date is immutable — only the stop list and settings can change. All supplied input fields replace the existing values; omitted fields keep their current values. If andFutureRoutesInSeries is true, the update is propagated to all future routes sharing the same series key. A new series key is generated and stamped on all affected routes (the edited route and all futures). Routes before the edit date keep the old series key. The route must belong to a series (non-null seriesKey) when andFutureRoutesInSeries is true — otherwise an error is returned. A one-off update (andFutureRoutesInSeries: false or omitted) clears the series key on the edited route only, detaching it from the series. Requires calendar.UPDATE. """ updateRoute( routeId: SafeInt! input: UpdateRouteInput! """ Number of weeks between recurrences. Omit to keep existing series membership. """ repeatEveryWeeks: Int """ Controls assignment propagation across future route occurrences. Only applies when repeatEveryWeeks is set. See RepeatAssignmentOption. """ repeatAssignment: RepeatAssignmentOption = OFF """ If true, the configured optimizer reorders stops and eagerly populates directions fields. """ optimize: Boolean """ Routing provider for this request. Defaults to the configured provider. """ provider: RoutingProvider """ If true, propagates this update to all future routes in the same series. Generates a new series key for all affected routes. Requires the route to have a non-null seriesKey. """ andFutureRoutesInSeries: Boolean ): Route! """ Deletes a route (and its stops). Idempotent — deleting an already-deleted or non-existent route returns deletedCount: 0 rather than erroring. If andFutureRoutesInSeries is true, also deletes every future route sharing the same series key. Requires calendar.UPDATE. """ deleteRoute( routeId: SafeInt! """If true, also deletes all future routes sharing the same series key.""" andFutureRoutesInSeries: Boolean ): DeleteRouteResult! """ Deprecated — use createRoute or updateRoute instead. Creates or replaces the route for a given date and set of events. Requires calendar.UPDATE. """ saveRoute(date: LocalDate!, eventIds: [SafeInt!]!, crewId: SafeInt, departAt: LocalTime, startLocation: CoordinateInput, endLocation: CoordinateInput, repeatEveryWeeks: Int): Route! @deprecated(reason: "Use createRoute or updateRoute") """ Removes route stops for the given events. Cleans up any routes that become empty. If repeatEveryWeeks is set, also removes stops for future recurring instances. If date is provided, only removes stops for that specific route date. Requires calendar.UPDATE. """ removeEventRoutes(eventIds: [SafeInt!]!, repeatEveryWeeks: Int, date: LocalDate): [Event!]! """ Removes a single event from all route stops on a specific date and cleans up empty routes. Use when an event is rescheduled away from a date (e.g. event update changes startDate). Requires calendar.UPDATE. """ removeEventRoute(eventId: SafeInt!, date: LocalDate!): Boolean """ Re-optimize an existing route in place, keeping its id. Reads the current stop list, re-orders it with the routing provider, and saves the new order with leg data already populated, so a following read makes no further provider call. Requires calendar.UPDATE. """ optimizeRoute( routeId: SafeInt! """ Routing provider for this request. Defaults to the configured provider. """ provider: RoutingProvider ): Route! """ Step 1 of subscription setup: creates a Stripe SetupIntent for securely collecting a new payment method. Requires role.OWNER. """ createStripeCompanySetupIntent: CompanyStripeSetupIntent! """ Step 2 of subscription setup: attaches the payment method from a completed SetupIntent to the company's Stripe customer. Requires role.OWNER. """ syncStripeCompanySetupIntent(setupIntentId: String!, billingAddress: BillingAddressInput!, setAsDefault: Boolean = true): StripePaymentMethod! """ Step 3 of subscription setup: creates a new Stripe subscription for the company at the given tier and billing interval. Requires role.OWNER. """ createPlanSubscription(tier: SubscriptionTier!, billingInterval: BillingInterval!, promoCode: String): Company! """ Immediately upgrades the subscription tier, or schedules a downgrade to take effect at the end of the current billing period. Requires role.OWNER. """ updatePlanSubscription(tier: SubscriptionTier!, billingInterval: BillingInterval!): Company! """ Clears any pending scheduled plan downgrade, keeping the current tier active. Requires role.OWNER. """ clearPendingPlanDowngrade: Company! """ Schedules the subscription to cancel at the end of the current billing period. Requires role.OWNER. """ cancelPlanSubscription(feedback: CancellationFeedbackInput!): Company! """ Clears a scheduled cancellation and keeps the subscription active. Requires role.OWNER. """ clearPendingPlanCancellation: Company! """ Pays the outstanding invoice of a past-due subscription and restores the account; use it when billingAction is RESUME. Optionally takes the payment method to pay with (and keep as the default); omit it to pay with the card on file. Works during the grace period and after the account has expired. Never changes the plan or a scheduled cancellation. Returns the company. Succeeds without charging when nothing is owed. Fails with extensions.code PAYMENT_FAILED (declineCode in extensions when Stripe gives one), NOT_RECOVERABLE (no subscription to resume; choose a plan instead) or INVALID_PAYMENT_METHOD. Requires role.OWNER. """ resumeSubscription(paymentMethodId: String): Company! """ Deprecated: use resumeSubscription. Now a thin wrapper over it, kept so a frontend deployed before resumeSubscription keeps working: it pays the outstanding invoice, restores the account and saves the card as resumeSubscription does, and reports the outcome as success/error/declineCode instead of returning the company or throwing. Requires role.OWNER. """ retrySubscriptionPayment(paymentMethodId: String): RetryPaymentResult! @deprecated(reason: "Use resumeSubscription") """Generate test properties for a customer from a predefined dataset""" generateTestProperties(customerId: Int!, dataset: PropertyDataSet!): [Property!]! """ Generate test events for a customer (one for every property the customer has). If customerId is omitted, generates events for all customers in the company. """ generateTestEvents(customerId: Int, startDate: LocalDate): [Event!]! """ Users with EDIT_OWN can only add entries for themselves. Users with EDIT_ALL can add entries for any user. Requires time_tracking.EDIT_OWN or higher. """ addTimeEntry(input: TimeEntryInput!): TimeEntry! """ Users with EDIT_OWN can only update their own entries. Users with EDIT_ALL can update entries for any user. Requires time_tracking.EDIT_OWN or higher. """ updateTimeEntry(timeEntryId: SafeInt!, input: UpdateTimeEntryInput!): TimeEntry! """ Explicitly clocks the current user into work and starts a global timer. This will return NULL if the user is/was already clocked in. This will throw an error if the pro timer feature is not enabled for this company. """ clockIn( """Optional parameter for the global timer category""" categoryId: SafeInt @deprecated location: LocationInput """Timestamp that the clock in action happened, if in the past""" at: DateTime ): ClockInOutAction """ Explicitly clocks the current user out of work and stops any running global or event timers. This will return NULL if the user was not clocked in """ clockOut( location: LocationInput """Timestamp that the clock out action happened, if in the past""" at: DateTime ): ClockInOutAction """ Accessible to all users. Users can clock themselves in. Clocking in another user requires timeTracking: EDIT_ALL. This will return NULL if the user is/was already clocked in. This will throw an error if the pro timer feature is not enabled for this company. """ clockInUser( userId: SafeInt! """Optional parameter for the global timer category""" categoryId: SafeInt @deprecated location: LocationInput """Timestamp that the clock in action happened, if in the past""" at: DateTime ): ClockInOutAction """ Accessible to all users. Users can clock themselves out. Clocking out another user requires timeTracking: EDIT_ALL. This will return NULL if the user was not clocked in """ clockOutUser( userId: SafeInt! location: LocationInput """Timestamp that the clock out action happened, if in the past""" at: DateTime ): ClockInOutAction """ Starts a global timer for the current user. If the user has an event timer or a global timer for another category, it will be stopped. If there is already a timer for this category or the company requires clock in and the user is not clocked in, this will throw an error. """ startGlobalTimer( categoryId: SafeInt location: LocationInput """Timestamp that the timer start happened, if in the past""" at: DateTime ): TimeEntry! @deprecated """ Starts an event timer for the current user. If the user has a global timer or timer for another event, it will be stopped. If there is already a timer for this event or the company requires clock in and the user is not clocked in, this will throw an error. """ startEventTimer( eventId: SafeInt! customerId: SafeInt @deprecated propertyId: SafeInt @deprecated location: LocationInput """Timestamp that the timer start happened, if in the past""" at: DateTime ): TimeEntry! """ Stops any running visit timer(s) for the current user. A running General Work timer is left running. If a visit timer is already closed and at is earlier than the stored end, the end is pulled back to at and the duration is recomputed. A timer that started after at is left running. A correction does not start General Work and does not emit another stop event. If the user is clocked in and has no timer, General Work is started and returned. """ stopTimer( location: LocationInput """Timestamp that the timer stop happened, if in the past""" at: DateTime ): TimeEntry """ Deletes a time entry. Users with EDIT_OWN can only delete their own entries. Users with EDIT_ALL can delete entries for any user. Cannot delete a running timer — stop it first. Requires timeTracking: EDIT_OWN or higher. """ deleteTimeEntry(timeEntryId: SafeInt!): Boolean! """ Deletes multiple time entries. Users with EDIT_OWN can only delete their own entries. Users with EDIT_ALL can delete entries for any user. Cannot delete running timers — stop them first. Requires timeTracking: EDIT_OWN or higher. """ deleteTimeEntries(timeEntryIds: [SafeInt!]!): Boolean! """This will create and continue a Compliance Inquiry.""" createTwilioComplianceInquiry(mockError: MockTwilioError): ComplianceInquiryResponse """ Submit an upsell for an event. Creates an upsell history record. If the upsell requires review, it will be marked as pending. Otherwise, SMS/email will be sent. """ submitUpsell(input: SubmitUpsellInput!): UpsellHistory! """ Creates or updates a user's capacity hours for a specific date. Use capacityHours: 0 to mark the user as unavailable. Requires users.UPDATE. """ overrideUserDayCapacity(userId: SafeInt!, date: Date!, capacityHours: Decimal!): UserDayOverride! """ Deletes a user's day capacity override by user and date. Requires users.UPDATE. """ deleteUserDayOverride(userId: SafeInt!, date: Date!): Boolean! """Creates a new user/employee. Requires users.CREATE.""" createUser(input: CreateUserInput!): User! """Deletes a user/employee. Requires users.DELETE.""" deleteUser(userId: SafeInt!): Boolean! """ Recalculates additional user seats based on current active users. Requires users.UPDATE. """ updateAdditionalUsersCount: Company! """Updates the schedule colors for the currently authenticated user.""" setScheduleColors(input: SetScheduleColorsInput!): User! } """ A date string, such as 2007-12-03, compliant with the `full-date` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. """ scalar Date """ A time string at UTC, such as 10:15:30Z, compliant with the `full-time` format outlined in section 5.6 of the RFC 3339profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. """ scalar Time """ A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. """ scalar DateTime """ A local date string (i.e., with no associated timezone) in `YYYY-MM-DD` format, e.g. `2020-01-01`. """ scalar LocalDate """ A local time string (i.e., with no associated timezone) in 24-hr `HH:mm[:ss[.SSS]]` format, e.g. `14:25` or `14:25:06` or `14:25:06.123`. """ scalar LocalTime """ A local date-time string (i.e., with no associated timezone) in `YYYY-MM-DDTHH:mm:ss` format, e.g. `2020-01-01T00:00:00`. """ scalar LocalDateTime """ The `BigInt` scalar type represents non-fractional signed whole numeric values. """ scalar BigInt """ The `SafeInt` scalar type represents non-fractional signed whole numeric values that are considered safe as defined by the ECMAScript specification. """ scalar SafeInt @specifiedBy(url: "https://www.ecma-international.org/ecma-262/#sec-number.issafeinteger") """The `Decimal` scalar type to represent precise numeric values.""" scalar Decimal """ The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). """ scalar JSON @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf") """ Generic sort order These are lowercase to match the Prisma enum names without needing to convert them in each resolver """ enum SortOrder { asc desc } """Print style for estimate PDFs: clean (no stub) or with payment stub.""" enum EstimatePrintStyle { CLEAN WITH_PAYMENT_STUB } """Generic filters for string fields""" input StringFilter { equals: String not: String in: [String!] notIn: [String!] lt: String lte: String gt: String gte: String contains: String startsWith: String endsWith: String } """Generic filters for number fields""" input IntFilter { equals: SafeInt not: SafeInt in: [SafeInt!] notIn: [SafeInt!] lt: SafeInt lte: SafeInt gt: SafeInt gte: SafeInt } """Generic filters for decimal fields""" input DecimalFilter { equals: Decimal not: Decimal in: [Decimal!] notIn: [Decimal!] lt: Decimal lte: Decimal gt: Decimal gte: Decimal } """ Generic filters for calendar date fields (YYYY-MM-DD). Use this for fields backed by a DB DATE column — accepts Date scalar values (e.g. \\"2026-06-01\\") and rejects full ISO timestamps, preventing timezone-shift bugs. """ input DateFilter { equals: Date not: Date in: [Date!] notIn: [Date!] lt: Date lte: Date gt: Date gte: Date } """ Generic filters for datetime fields using ISO format YYYY-MM-DDTHH:MM:SSZ """ input DateTimeFilter { equals: DateTime not: DateTime in: [DateTime!] notIn: [DateTime!] lt: DateTime lte: DateTime gt: DateTime gte: DateTime } enum ObjectType { ADMIN COMPANY CREW CUSTOMER CUSTOMER_PAYMENT_METHOD EQUIPMENT ESTIMATE EVENT INVOICE ITEM PAYMENT PROPERTY RATING RECURRING_EVENT TIME_ENTRY UNKNOWN USER VENDOR WORK_REQUEST } enum DepositRequirement { NONE ALL PER_CUSTOMER } enum InvoiceGrouping { PER_VISIT BY_CUSTOMER BY_PROPERTY BY_PROPERTY_AND_ASSET } enum InvoiceDatePolicy { DATE_SENT LAST_SERVICE_DATE NO_CHANGE } type CustomNaming { customer: String customers: String invoice: String invoices: String estimate: String estimates: String employee: String employees: String crew: String crews: String itemsAndService: String itemsAndServices: String equipment: String equipments: String tax: String taxes: String } type Company { id: SafeInt! name: String! email: String! isBanned: Boolean! @deprecated(reason: "Use accountStatus field instead") isActive: Boolean @deprecated(reason: "Use accountStatus field instead") accountStatus: AccountStatus! """Subscription details (plan, status, billing lifecycle)""" subscription: CompanySubscription address: Address! """ The company's main address, geocoded as a coordinate pair. Null if not yet geocoded. """ location: Location zip: String! phone: String mobile: String tier: Tier! @deprecated(reason: "Use subscription.tier instead") paypalEmail: String! isPaypalEnabled: Boolean! fax: String timezone: String! dformat: String! @deprecated(reason: "use dateFormat") tformat: String! @deprecated(reason: "use timeFormat") dsep: String! @deprecated(reason: "use decimalSeparator") tsep: String! @deprecated(reason: "use thousandsSeparator") dateFormat: String! timeFormat: String! decimalSeparator: String! thousandsSeparator: String! currency: String latitude: String @deprecated(reason: "Use location instead") longitude: String @deprecated(reason: "Use location instead") logo: File! www: String! stripeCustomerId: String! showInvoiceStatus: SafeInt! hideInvoiceOutstandingBalance: SafeInt! invoiceDueDate: SafeInt! profilePic: String! facebook: String! twitter: String! instagram: String! googleReviewLink: String showTips: Boolean! showCustomFields: Boolean! """ Which days the company operates — affects event scheduling and recurrence """ workDays: CompanyWorkDays! """ What happens to recurring (and any other system-generated) events that fall on an off day (only relevant when workDays ≠ WORK_ALL_DAYS) """ workDaysAction: CompanyWorkDaysAction! depositRequirement: DepositRequirement! depositThreshold: Decimal! depositAmount: Decimal! depositPercentage: Boolean! paymentMethodRequired: Boolean! showAddCredit: Boolean! settings: CompanySettings customNaming: CustomNaming plan: Plan @deprecated(reason: "Use subscription.planPriceId instead") pendingPlan: Plan @deprecated(reason: "Use subscription.pendingPriceId instead") """Is the Stripe subscription cancelled by the user""" isPlanCancelled: Boolean! @deprecated(reason: "Use accountStatus field instead") """Whether the subscription payment failed or lapsed after cancellation""" isPlanExpired: Boolean! @deprecated(reason: "Use accountStatus field instead") """Date the current subscription expires (if cancelled) or renews""" planPeriodEndsAt: DateTime @deprecated(reason: "Use subscription.planPeriodEndsOn instead") stripeSubscription: JSON stripeSubscriptions: [JSON!]! """Date the subscription started""" registrationDate: DateTime! """Number of currently active users""" activeUsers: SafeInt! """Number of additional user seats purchased""" userSeatsPurchased: Int! """ Default payment method on the company's platform Stripe customer (Copilot billing). Same `StripePaymentMethod` shape as client-portal saved methods, singular. """ stripeDefaultPaymentMethod: StripePaymentMethod stamps: SafeInt! """Twilio/SMS configuration (read-only)""" twilio: Twilio! """QuickBooks status (read-only, null if not connected)""" quickbooks: QuickBooks taxId: String! abn: String! """ If company has not yet finished signup and filled out their contact information """ isFirst: Boolean! @deprecated(reason: "Use accountStatus field instead") """ When true, the company's main business address is used as both the route start depot and end depot, overriding routeStartLocation and routeEndLocation. When false (the default), routeStartLocation and routeEndLocation are used when set; the company's main address is the fallback when they are null. """ useCompanyAddressAsDepot: Boolean! """ Custom route start location (depot). Null when coords are not set (or are the default 0/0), or when useCompanyAddressAsDepot is true (in which case the company's main address is used instead). """ routeStartLocation: Location """ Custom route end location. Null when coords are not set, or when useCompanyAddressAsDepot is true. When null and useCompanyAddressAsDepot is false, route optimization falls back to routeStartLocation, then the company's main address. """ routeEndLocation: Location """ Fully resolved depot start location, applying the same fallback chain as the route engine (useCompanyAddressAsDepot → stored coords → geocode name → company main address). Null only if nothing is configured and geocoding fails. Use this (not routeStartLocation) to place the depot marker on a map. """ resolvedRouteStartLocation: Location """ Fully resolved depot end location. Falls back through the same chain as resolvedRouteStartLocation. Null only if nothing is configured and geocoding fails. """ resolvedRouteEndLocation: Location """Whether the Price Matrix feature is enabled for this company""" isPriceMatrixEnabled: Boolean! apiTrial: ApiTrial } type CompanySettings { id: SafeInt! invoiceNotes: String! invoiceTerms: String! estimateNotes: String! estimateTerms: String! weekStart: SafeInt! colorClose: String! colorVisit: String! colorTask: String! colorMeet: String! colorMaint: String! colorInvoice: String! invoiceGrouping: InvoiceGrouping! invoiceDueDate: String! invoicePastDueLateFee: String! invoiceTermsRed: Boolean! estimateTermsRed: Boolean! defaultInvoiceServiceDate: Boolean showLivechat: Boolean! showGettingStarted: Boolean! showWrsd: JSON hideWrf: Boolean! invoiceDatePolicy: InvoiceDatePolicy! eventInvoiceDateSource: EventInvoiceDateSource! enablePublicWrf: Boolean! workingHours: String! showEstimateBudgetedHours: Boolean showInvoiceBudgetedHours: Boolean showEstimateQuantityPerHour: Boolean showInvoiceQuantityPerHour: Boolean showEstimateCostRate: Boolean showInvoiceCostRate: Boolean } """QuickBooks status (read-only, null if not connected)""" type QuickBooks { isConnected: Boolean! invoicesLastDownloaded: DateTime invoicesSyncFailedCount: Int! customersLastDownloaded: DateTime customersSyncFailedCount: Int! paymentsLastDownloaded: DateTime paymentsSyncFailedCount: Int! expiredAt: DateTime } """ Controls which date is used on invoice line items when an event is invoiced. - SCHEDULED_DATE: use the event's scheduled date. - CLOSED_DATE: use the date the event was closed (closedAt) as the invoice line item date. - PROMPT_USER: ask the user to choose at close time (only shown when today ≠ scheduled date and the event is invoiceable). """ enum EventInvoiceDateSource { SCHEDULED_DATE CLOSED_DATE PROMPT_USER } """ What happens to events that fall on a company off day. Only relevant when workDays ≠ WORK_ALL_DAYS. """ enum CompanyWorkDaysAction { """Skip (do not create) the event if it falls on an off day""" SKIP """Move the event to the preceding Friday""" MOVE_TO_FRIDAY """Move the event to the following Monday""" MOVE_TO_MONDAY """Move to Friday if it falls on Saturday, Monday if it falls on Sunday""" MOVE_TO_FRIDAY_OR_MONDAY } """ Which days of the week the company operates. Controls whether events fall on off-days and how recurrences are scheduled. """ enum CompanyWorkDays { """Work all 7 days a week""" WORK_ALL_DAYS """No Saturdays, work Sundays""" NO_WORK_ON_SATURDAY """No Sundays, work Saturdays""" NO_WORK_ON_SUNDAY """No Saturdays or Sundays""" NO_WORK_WEEKEND } """ Account status - controls app access (ACTIVE, PAST_DUE, EXPIRED, CANCEL_REQUESTED, CANCELLED, BANNED, INACTIVE, SIGNUP_INCOMPLETE) """ enum AccountStatus { ACTIVE PAST_DUE EXPIRED CANCEL_REQUESTED CANCELLED BANNED INACTIVE SIGNUP_INCOMPLETE } """Twilio/SMS configuration (read-only)""" type Twilio { number: String! sms: Boolean! unlimitedTexts: Boolean periodStart: DateTime expiredAt: DateTime """Subaccount SID (admin); null if not set""" subAccountSid: String """Messaging service SID / A2P (admin); null if not set""" messagingServiceSid: String """ Whether an encrypted subaccount auth token is stored (token is never exposed) """ hasSubAccountToken: Boolean! status: TwilioStatus! } """Status of a company's Twilio SMS setup""" enum TwilioStatus { NOT_STARTED PENDING ACTIVE CANCELED } """Deprecated. Use Crew.users instead.""" type CrewMember { id: SafeInt! crewId: SafeInt! userId: SafeInt! user: User! } type Crew { id: SafeInt! name: String! description: String notes: String! lp4Id: String! isDeleted: Boolean! deletedAt: DateTime tags: [String!]! color: String eventCount(where: EventFilter): SafeInt! events(where: EventFilter = {isDeleted: false}, orderBy: [EventSort!], take: SafeInt = 1000, skip: SafeInt): [Event!] users(where: UserFilter = {isDeleted: false}, orderBy: [UserSort!], take: SafeInt = 1000, skip: SafeInt): [User!] """Deprecated. Use users instead.""" members(take: SafeInt = 1000, skip: SafeInt): [CrewMember!] @deprecated(reason: "Use users") """ Stored start coordinates and name. Null when this end has no usable coordinates (missing, or 0,0), even if a name is stored. """ routeStartLocation: Location """ Stored end coordinates and name. Null when this end has no usable coordinates (missing, or 0,0), even if a name is stored. """ routeEndLocation: Location } input CrewFilter { id: IntFilter name: StringFilter isDeleted: Boolean events: EventFilter AND: [CrewFilter!] OR: [CrewFilter!] NOT: CrewFilter } enum CustomFieldType { NONE DATE CHECKBOX DEC INT TEXT } """ Owner type a custom-field definition applies to. Whitelist of real DB enum members — excludes the legacy bogus lowercase `invoice` value (INVOICE_DEPRECATED in Prisma), which was never used on DynamicCustomField. """ enum CustomFieldObjectType { ASSET CHEMAPP CHEMICAL CUSTOMER EQUIPMENT ESTIMATE EVENT INVOICE RECURRING VENDOR } type DynamicCustomField { id: SafeInt! name: String! type: CustomFieldType! """ Which object type this definition applies to (e.g. EVENT, CUSTOMER, ASSET). """ objectType: CustomFieldObjectType! """ Default configured for this field, as stored text. Null when none is set. TEXT is the literal, INT and CHECKBOX are whole numbers ("123", "1"), DEC is a decimal ("123.45"), and DATE is the stored date string. Clients apply this when the matching CustomField value slot is null. """ defaultValue: String } type CustomField { id: SafeInt! name: String type: CustomFieldType """ Null when this field is not TEXT, or when the stored text is empty (unset or cleared). Fall back to dynamicCustomField.defaultValue. """ textValue: String """ Null when this field is not INT or CHECKBOX, or when the stored number is 0 (unset, unchecked, or cleared). Fall back to dynamicCustomField.defaultValue. """ integerValue: SafeInt """ Null when this field is not DEC, or when the stored number is zero. Fall back to dynamicCustomField.defaultValue. """ decimalValue: Decimal dateValue: Date dynamicCustomField: DynamicCustomField } """ Input for setting a custom field value on create/update. Set exactly the value field matching the DynamicCustomField's type (TEXT → textValue, INT and CHECKBOX → integerValue, DEC → decimalValue, DATE → dateValue); pass it as null to clear the field. Leaving it out, or setting a different value field, is a 400. `intValue` is a deprecated alias for integerValue; sending both with different values is a 400. Values must fit the column: INT is 32-bit, CHECKBOX is 0 or 1, DEC is under 100,000,000 with at most 2 decimal places, TEXT is at most 255 characters. """ input CustomFieldInput { """ID of the DynamicCustomField definition for this company.""" fieldId: SafeInt! textValue: String """ Value for INT and CHECKBOX fields (CHECKBOX: 0 or 1). Null clears the field. """ integerValue: SafeInt intValue: SafeInt @deprecated(reason: "Use integerValue.") decimalValue: Decimal dateValue: LocalDate } enum AutoChargePolicy { """ Default/unconfigured state — customer inherits company auto-charge settings. Functionally identical to USE_COMPANY at runtime; distinguished only by whether the setting was explicitly configured or simply never set. """ INHERIT """ Auto-charge explicitly disabled for this customer, regardless of company settings. """ DISABLED """ Explicitly set to follow company-level auto-charge settings. Functionally identical to INHERIT at runtime; distinguished only by whether the setting was explicitly configured or simply never set. """ USE_COMPANY } """Status of a customer account.""" enum CustomerStatus { INACTIVE ACTIVE WRITTEN_OFF PENDING_APPROVAL } input CustomerStatusFilter { equals: CustomerStatus not: CustomerStatus in: [CustomerStatus!] notIn: [CustomerStatus!] } type CustomerType { id: SafeInt! name: String! } type Customer { id: SafeInt! """Contact email""" email: String! ccEmail: String! ccEmail1: String! ccEmail2: String! ccEmail3: String! fullName: String! firstName: String! lastName: String! dateRegistered: DateTime address: Address! """ The customer's address, geocoded as a coordinate pair. Null if not yet geocoded. """ location: Location """Business or landline phone number.""" phone: String! fax: String! """ Mobile/cell phone number. This is the field to set for SMS — always populate this when SMS is needed. For US customers, this value is passed to Twilio directly. For international customers, SMS uses cellFull (which is auto-computed from this field + country). """ cell: String! customerType: CustomerType customerTypeId: SafeInt sourceId: SafeInt status: CustomerStatus! description: String! isDeleted: Boolean! discountPercent: Decimal! credit: Decimal! """Used for client portal URL""" portalKey: String! lastLogin: DateTime outstanding: Decimal! lifetimePaid: Decimal! pastDue: Decimal! pic: String! createdAt: DateTime """Timestamp of the last write to this customer record.""" updatedAt: DateTime """ QuickBooks sync tracking field — not a general-purpose update timestamp. Use updatedAt for incremental sync. """ quickbooksModifiedAt: DateTime """ @deprecated Use updatedAt for incremental sync. quickbooksModifiedAt for the QuickBooks-specific field. """ lastUpdated: DateTime @deprecated(reason: "QuickBooks sync field, not a reliable update timestamp. Use updatedAt for incremental sync.") """Website""" www: String! title: String! invoiceTerms: String! number: String! """ Read-only. Computed E.164-style number (digits only, no '+') derived from cell + country. Examples: '15551234567' (US), '61412345678' (AU). Used for outbound SMS to international customers and for inbound SMS matching. Do not set this directly — set cell (and ensure country is correct) and it is computed automatically. """ cellFull: String! tags: [String!]! portalNeedsLogin: Boolean! """Email address used for login""" loginEmail: String! """Whether customer receives invoices via phone""" isReceivePhone: Boolean! """Whether customer receives invoices via email""" isReceiveEmail: Boolean! """Whether customer receives invoices via text""" isReceiveText: Boolean! hasPaymentMethod: Boolean hideInvoiceOutstandingBalance: SafeInt! showTotalDue: Boolean showInvoiceStatus: SafeInt! """Days past due?""" pastDueTerms: SafeInt! pastDueValue: Decimal! pastDueValueType: String! invoiceDueDate: SafeInt! """Show customer's available credit""" showCreditAvailable: Boolean! autoCreditApply: Boolean! autoChargePolicy: AutoChargePolicy! autoChargeEnabledAt: DateTime showTips: Boolean! deletedAt: DateTime pastDueTermsConvertAt: DateTime pastDueTermsConvertType: String pastDueTermsSettingsArchive: String allowAch: Boolean! ratingRequestSentAt: DateTime """Toggled in the customer settings""" portalNeedsPaymentMethod: Boolean! """Derived field that takes into account company settings""" paymentMethodRequired: Boolean! depositThreshold: Decimal! depositAmount: Decimal! depositPercentage: Boolean! """Whether the customer has opted in to receive marketing emails""" receiveMarketingEmails: Boolean! """ Custom field values set for this customer. Seeded for every DynamicCustomField that applies to customers. """ customFields: [CustomField!]! properties(where: PropertyFilter, orderBy: [PropertySort!], take: SafeInt = 1000, skip: SafeInt): [Property!] source: CustomerSource """ Whether email can be sent to this customer: UNSUBSCRIBED (has an unsubscribe tag), NO_EMAIL (blank email) or SENDABLE """ emailSendability: CustomerEmailSendability! """ Whether SMS can be sent to this customer. This is recipient-level preflight; company setup and provider-level validation are enforced by the send operation. """ smsSendability: CustomerSmsSendability! eventCount(where: EventFilter): SafeInt! """Customer's payment history""" payments(where: PaymentFilter = {isDeleted: false}, orderBy: [PaymentSort!], take: SafeInt = 1000, skip: SafeInt): [Payment!]! stripePaymentMethods: [StripePaymentMethod!]! """ Stripe Sources (their legacy API for payment methods). This is currently the only way to know if a bank account is verified. """ stripePaymentSources(type: StripePaymentSourceType): [StripePaymentSource!]! """ Invoices for this customer. Defaults to non-archived, active invoices. Pass where: { isArchived: true } to include archived, or where: { deletedState: { equals: DELETED } } to include soft-deleted. """ invoices(where: InvoiceFilter = {isArchived: false, deletedState: {equals: ACTIVE}}, orderBy: [InvoiceSort!], take: SafeInt = 1000, skip: SafeInt): [Invoice!]! """ Estimates for this customer. Defaults to non-archived, non-deleted estimates. """ estimates(where: EstimateFilter = {isArchived: false, isDeleted: false}, orderBy: [EstimateSort!], take: SafeInt = 1000, skip: SafeInt): [Estimate!]! """ Events (jobs/visits) for this customer. Defaults to non-deleted events. """ events(where: EventFilter = {isDeleted: false}, orderBy: [EventSort!], take: SafeInt = 1000, skip: SafeInt): [Event!]! } input TagsFilter { in: [String!] notIn: [String!] } input CustomerFilter { id: IntFilter email: StringFilter fullName: StringFilter firstName: StringFilter lastName: StringFilter dateRegistered: DateTimeFilter address: AddressFilter phone: StringFilter fax: StringFilter cell: StringFilter """ Cell number including country code. Not a canonical format: rows may be stored with or without a leading '+' (e.g. '15551234567' or '+15551234567'), or as raw digits when the country is unknown. Use contains, not startsWith, when matching digits a user typed. """ cellFull: StringFilter customerTypeId: IntFilter status: CustomerStatusFilter description: StringFilter tags: TagsFilter isDeleted: Boolean events: EventFilter """ Filter by last-write timestamp. Use for "changed since X" incremental sync. """ updatedAt: DateTimeFilter AND: [CustomerFilter!] OR: [CustomerFilter!] NOT: CustomerFilter } input CustomerSort { id: SortOrder firstName: SortOrder lastName: SortOrder fullName: SortOrder email: SortOrder updatedAt: SortOrder createdAt: SortOrder } type Document { id: SafeInt! companyId: SafeInt! description: String! text: String! date: DateTime! customerId: SafeInt name: String! isArchived: Boolean! sentEmail: Boolean! snailMail: Boolean! view: Boolean } """A piece of equipment or vehicle belonging to a company.""" type Equipment { id: SafeInt! companyId: SafeInt! crewId: SafeInt name: String type: Int status: Int } input EstimateReportOrderBy { status: SortOrder customerId: SortOrder date: SortOrder isSent: SortOrder _count: EstimateReportCountOrderBy _sum: EstimateReportSumOrderBy _avg: EstimateReportAvgOrderBy _min: EstimateReportMinOrderBy _max: EstimateReportMaxOrderBy count: EstimateReportCountOrderBy @deprecated(reason: "Use _count") sum: EstimateReportSumOrderBy @deprecated(reason: "Use _sum") avg: EstimateReportAvgOrderBy @deprecated(reason: "Use _avg") min: EstimateReportMinOrderBy @deprecated(reason: "Use _min") max: EstimateReportMaxOrderBy @deprecated(reason: "Use _max") } input EstimateReportCountOrderBy { id: SortOrder } input EstimateReportSumOrderBy { subtotal: SortOrder total: SortOrder tax: SortOrder discountAmount: SortOrder profit: SortOrder budgetedHoursTotal: SortOrder budgetedHoursCost: SortOrder } input EstimateReportAvgOrderBy { subtotal: SortOrder total: SortOrder tax: SortOrder discountAmount: SortOrder profit: SortOrder budgetedHoursTotal: SortOrder budgetedHoursCost: SortOrder } input EstimateReportMinOrderBy { date: SortOrder subtotal: SortOrder total: SortOrder tax: SortOrder discountAmount: SortOrder profit: SortOrder budgetedHoursTotal: SortOrder budgetedHoursCost: SortOrder } input EstimateReportMaxOrderBy { date: SortOrder subtotal: SortOrder total: SortOrder tax: SortOrder discountAmount: SortOrder profit: SortOrder budgetedHoursTotal: SortOrder budgetedHoursCost: SortOrder } type EstimateReportCountAggregate { id: SafeInt! } type EstimateReportSumAggregate { subtotal: Decimal total: Decimal tax: Decimal discountAmount: Decimal profit: Decimal budgetedHoursTotal: Decimal budgetedHoursCost: Decimal } type EstimateReportAvgAggregate { subtotal: Decimal total: Decimal tax: Decimal discountAmount: Decimal profit: Decimal budgetedHoursTotal: Decimal budgetedHoursCost: Decimal } type EstimateReportMinAggregate { date: Date subtotal: Decimal total: Decimal tax: Decimal discountAmount: Decimal profit: Decimal budgetedHoursTotal: Decimal budgetedHoursCost: Decimal } type EstimateReportMaxAggregate { date: Date subtotal: Decimal total: Decimal tax: Decimal discountAmount: Decimal profit: Decimal budgetedHoursTotal: Decimal budgetedHoursCost: Decimal } enum EstimateStatus { """Not yet sent to the customer""" DRAFT """Sent to the customer, awaiting their decision""" PENDING """Customer accepted — ready to schedule and invoice""" ACCEPTED """Customer declined the estimate""" DECLINED """An invoice has been created from this estimate""" INVOICED """Customer has requested changes (shown as 'Changes Requested' in UI)""" CHANGE_REQUESTED """ Changes have been applied in response to a change request (shown as 'Changes Updated' in UI) """ CHANGE_UPDATED } enum EstimateChangeRequested { NO_CHANGE_REQUESTED CHANGE_APPLIED CHANGE_REQUESTED } type Estimate { id: SafeInt! number: SafeInt! purchaseOrder: String! date: Date! status: EstimateStatus! title: String notes: String! terms: String! subtotal: Decimal! discountPercent: Decimal! discountAmount: Decimal! tax: Decimal! """The total cost of this estimate, including subtotal, discounts and tax""" total: Decimal! """The total less any previous payments for this estimate""" totalOwed: Decimal! """The sum of all deposit payments that have been made for this estimate""" paidAmount: Decimal! """The depositRequirement less any previous payments for this estimate""" depositOwed: Decimal! """ The required deposit in order to accept this estimate, based on company and customer settings """ depositRequirement: Decimal! payInFullEnabled: Boolean! isArchived: Boolean! hasBeenViewed: Boolean! requested: EstimateChangeRequested! requestedChanges: String createdAt: DateTime updatedAt: DateTime requestedAt: DateTime acceptedAt: DateTime lineItems(where: EstimateLineItemFilter, orderBy: [EstimateLineItemSort!] = [{sortOrder: asc}, {id: asc}]): [EstimateLineItem!]! media(where: MediaWhere, orderBy: [MediaSort!], take: SafeInt = 1000, skip: SafeInt): [Media!]! history(where: EstimateHistoryFilter, orderBy: [EstimateHistorySort!], take: SafeInt = 1000, skip: SafeInt): [EstimateHistory!]! customFields: [CustomField!]! customerId: SafeInt! customer: Customer! internalNotes: String isSent: Boolean! sentEmail: Boolean! sentSms: Boolean! sentSnailMail: Boolean! invoiceId: SafeInt! requestId: SafeInt invoiceNumber: SafeInt! isDeleted: Boolean! tags: String! emailSubject: String! emailMessage: String! droppedOffAtProperty: Boolean! emailOpened: Boolean! sentCopyLink: Boolean! budgetedHoursTotal: Decimal budgetedHoursCost: Decimal profit: Decimal visitCost: Decimal """ User ID of the estimator who created this estimate. Null when the creator was not recorded. """ createdByUserId: SafeInt """ User who created this estimate. Null when createdByUserId is null or the user no longer exists. Soft-deleted users still resolve for historical accuracy. """ createdByUser: User } input EstimateStatusFilter { equals: EstimateStatus not: EstimateStatus in: [EstimateStatus!] notIn: [EstimateStatus!] } input EstimateFilter { id: IntFilter estimateId: IntFilter number: IntFilter purchaseOrder: StringFilter propertyId: IntFilter date: DateFilter status: EstimateStatusFilter title: StringFilter notes: StringFilter terms: StringFilter isArchived: Boolean isDeleted: Boolean subtotal: DecimalFilter discountPercent: DecimalFilter discountAmount: DecimalFilter tax: DecimalFilter total: DecimalFilter hasBeenViewed: Boolean requestedChanges: StringFilter createdAt: DateTimeFilter updatedAt: DateTimeFilter requestedAt: DateTimeFilter """Has accepted line items that have not been scheduled""" hasUnscheduledLineItems: Boolean AND: [EstimateFilter!] OR: [EstimateFilter!] NOT: [EstimateFilter!] customerId: IntFilter customer: CustomerFilter internalNotes: StringFilter isSent: Boolean invoiceId: IntFilter sentCopyLink: Boolean sentEmail: Boolean sentSnailMail: Boolean sentSms: Boolean budgetedHoursTotal: DecimalFilter budgetedHoursCost: DecimalFilter acceptedAt: DateTimeFilter } enum EstimateLineItemRemoved { """Included on the estimate. Prefer isIncluded.""" RETAINED """Unchecked / excluded from the estimate. Prefer isIncluded: false.""" REMOVED """Included on the estimate (historical default). Prefer isIncluded.""" RETAINED_AND_ACCEPTED } input EstimateLineItemRemovedFilter { equals: EstimateLineItemRemoved not: EstimateLineItemRemoved in: [EstimateLineItemRemoved!] notIn: [EstimateLineItemRemoved!] } type EstimateLineItem { id: SafeInt! eventId: SafeInt eventIsRecurring: Boolean! date: Date! eventItemId: SafeInt itemId: SafeInt name: String! description: String! """Sort order of the line item""" sortOrder: SafeInt! price: Decimal! cost: Decimal! @deprecated(reason: "Use price instead") quantity: Decimal! """ Estimated hours for this line item. Not matrix-specific — can be set on any line item. Per-visit value; for matrix lines, total hours across all visits = budgetedHours × visitCount. """ budgetedHours: Decimal taxPercent1: Decimal! taxPercent2: Decimal! taxPrice1: Decimal! taxPrice2: Decimal! subtotal: Decimal! subtotalWithTax: Decimal! subtotalWithDiscount: Decimal! propertyId: SafeInt property: Property isRecommended: Boolean! """Legacy inclusion column. Prefer isIncluded.""" removed: EstimateLineItemRemoved! @deprecated(reason: "Use isIncluded instead.") """ Whether the line is included in estimate totals / the invoice (checkbox state). True when removed is RETAINED or RETAINED_AND_ACCEPTED; false when REMOVED. For partial-accept workflows: pair with Estimate.status — on an ACCEPTED or INVOICED estimate, isIncluded true means the line was kept; false means it was excluded. On open estimates, this is only the checkbox, not a final outcome. """ isIncluded: Boolean! """ Number of visits the rate matrix priced this line item for. Null/0 on non-matrix lines. """ visitCount: SafeInt! """ Authoritative customer-facing subtotal for matrix lines. Derived as visitCount × perVisitRate (mode 0) or visitCount × rm_rate × calc (mode 1). Per-visit price = priceAllVisits ÷ visitCount. Null/0 on non-matrix lines. """ priceAllVisits: Decimal! """ Internal company labour rate ($/hr) from the rate matrix — not customer-facing. Null/0 on non-matrix lines. """ internalHourlyRate: Decimal! """ The custom-field measurement value (e.g. square footage, lot size) used as input to the rate matrix lookup. Null on non-matrix lines. """ matrixCalcInput: String """ FK to the rate matrix config (price_matrices_items) that priced this line. Null = not a matrix line. """ matrixItemId: SafeInt estimateId: SafeInt! estimate: Estimate! item: Item """AKA scheduledStatus""" eventStatus: EstimateLineItemEventStatus! """ Finds the most efficient ways to insert this line item's property location into existing routes, grouped by day. Evaluates the startDate–endDate range (defaults to the line item's scheduled date). Returns an empty list if the line item has no property with valid coordinates. """ routeSuggestions( """ Start of the date range to evaluate (inclusive, YYYY-MM-DD). Defaults to the line item's scheduled date. """ startDate: LocalDate """ End of the date range to evaluate (inclusive, YYYY-MM-DD). Defaults to startDate. """ endDate: LocalDate """ Filter by crew, customer, or other event properties. Does not affect the date range. """ eventFilter: EventFilter """ If true, use crow-flies for all calculations. No directions API calls made. """ fastMode: Boolean ): [SchedulingDateSuggestion!]! } input EstimateLineItemFilter { id: IntFilter estimateId: IntFilter estimate: EstimateFilter date: DateFilter itemId: IntFilter item: ItemFilter name: StringFilter description: StringFilter price: DecimalFilter quantity: DecimalFilter budgetedHours: DecimalFilter subtotal: DecimalFilter subtotalWithTax: DecimalFilter subtotalWithDiscount: DecimalFilter propertyId: IntFilter property: PropertyFilter removed: EstimateLineItemRemovedFilter """ Filter by inclusion checkbox. True = included in totals; false = excluded. """ isIncluded: Boolean AND: [EstimateLineItemFilter!] OR: [EstimateLineItemFilter!] NOT: [EstimateLineItemFilter!] eventId: IntFilter eventIsRecurring: Boolean eventItemId: IntFilter eventStatus: EstimateLineItemEventStatusFilter } input EstimateLineItemSort { id: SortOrder estimateId: SortOrder estimate: EstimateSort propertyId: SortOrder property: PropertySort date: SortOrder itemId: SortOrder item: ItemSort name: SortOrder description: SortOrder sortOrder: SortOrder price: SortOrder quantity: SortOrder budgetedHours: SortOrder subtotal: SortOrder subtotalWithTax: SortOrder subtotalWithDiscount: SortOrder } input EstimateActionFilter { equals: EstimateAction not: EstimateAction in: [EstimateAction!] notIn: [EstimateAction!] } input EstimateActionReasonFilter { equals: EstimateActionReason not: EstimateActionReason in: [EstimateActionReason!] notIn: [EstimateActionReason!] } type EstimateHistory { id: SafeInt! date: DateTime action: EstimateAction actionReason: EstimateActionReason comment: String userId: SafeInt customerId: SafeInt user: EstimateHistoryUser customer: EstimateHistoryUser } input EstimateHistoryFilter { id: IntFilter estimateId: IntFilter action: EstimateActionFilter actionReason: EstimateActionReasonFilter date: DateTimeFilter } type EstimateHistoryUser { id: SafeInt! fullName: String } input EstimateSort { id: SortOrder title: SortOrder number: SortOrder purchaseOrder: SortOrder status: SortOrder date: SortOrder subtotal: SortOrder discountPercent: SortOrder discountAmount: SortOrder tax: SortOrder total: SortOrder createdAt: SortOrder updatedAt: SortOrder requestedAt: SortOrder customer: CustomerSort acceptedAt: SortOrder } input EstimateHistorySort { id: SortOrder date: SortOrder } enum EstimateActionReason { PRICING SCHEDULING OTHER } enum EstimateAction { CREATE ACCEPT DECLINE EMAIL EMAIL_OPEN VIEW UPDATE REQUEST_CHANGES SMS INVOICE ARCHIVE MARK_EMAIL MARK_NOTSENT DROPPED_OFF UNARCHIVE MAIL PENDING MARK_MAIL LINK_COPY } """Position of an event within its recurring series""" type RecurringEventPosition { """1-based position of this event in the series""" position: Int! """ Total number of events in the series. Null for indefinitely recurring series. """ total: Int } type Event { id: SafeInt! propertyId: SafeInt property: Property location: Location """ The event's own location text. Null when the event has no location text of its own. Unlike `location`, this is readable even when the event has no usable coordinates, so it is the safe field for an address label. """ locationName: String """Whether this event start and end have time of day vs only dates""" hasTime: Boolean! """Date of the event""" startDate: LocalDate! """ Time of the event in the company's timezone. Null when hasTime is false. """ startTime: LocalTime """Date and time of the event in the company's timezone""" startDateTime: LocalDateTime! """ Exact inclusive last active date for a multi-day event or waitlist scheduling window; null for a single-day event. """ endDate: LocalDate """ Time the event ends in the company's timezone. Should be considered 00:00:00 if hasTime is false """ endTime: LocalTime! """Date and time that the event ends in the company's timezone""" endDateTime: LocalDateTime title: String! recurringEventId: SafeInt recurringId: SafeInt @deprecated(reason: "Use recurringEventId instead") type: EventType! """ID of the linked work request, if any.""" workRequestId: SafeInt requestId: SafeInt @deprecated(reason: "Use workRequestId instead") description: String! cost: Decimal! @deprecated """The subtotal of this event: sum of all event items before tax""" subtotal: Decimal! """ The total cost of this event: subtotal with tax. Sum of 'total' for all non-deleted line items """ total: Decimal! equipmentId: SafeInt maintenanceCost: Decimal! isInvoiceable: Boolean! invoiceId: SafeInt invoice: Invoice budgetedHours: Decimal! """Use sortOrder instead""" sortId: SafeInt! @deprecated sortOrder: SafeInt! contractId: SafeInt isNotServiced: Boolean! @deprecated(reason: "Removing in favor of the 'status' column") isClosed: Boolean! @deprecated(reason: "Removing in favor of the 'status' column") isSkipped: Boolean! @deprecated(reason: "Removing in favor of the 'status' column") isCancelled: Boolean! @deprecated(reason: "Removing in favor of the 'status' column") isWaitlisted: Boolean! @deprecated(reason: "Removing in favor of the 'status' column") notServicedReason: String! @deprecated(reason: "Use skippedReason instead") """Date and time that the event was closed in UTC""" closedAt: DateTime """Date that the event was skipped in the company's timezone""" skippedDate: LocalDate """ Date and time that the event was skipped in UTC. Note: The CRM only sets the date portion. """ skippedAt: DateTime skippedReason: String programId: SafeInt estimateId: SafeInt categoryId: SafeInt beforeVisitCount: SafeInt! isDeleted: Boolean! createdAt: DateTime! updatedAt: DateTime estimate: Estimate lineItems(where: EventItemFilter = {isDeleted: false}, orderBy: [EventItemSort!], take: SafeInt = 1000, skip: SafeInt): [EventItem!]! """Use lineItems instead""" eventItems(where: EventItemFilter = {isDeleted: false}, orderBy: [EventItemSort!], take: SafeInt = 1000, skip: SafeInt): [EventItem!]! @deprecated(reason: "Use lineItems instead") media(where: MediaWhere, orderBy: [MediaSort!], take: SafeInt = 1000, skip: SafeInt): [Media!]! beforeAndAfters(where: BeforeAndAfterWhere, orderBy: [BeforeAndAfterSort!], take: SafeInt = 1000, skip: SafeInt): [BeforeAndAfter!]! status: EventStatus! """ Calculated status based on latest date (endDate or startDate): UPCOMING (>7 days or not set), DUE_SOON (within 7 days), OVERDUE (before today) """ waitlistDueStatus: WaitlistDueStatus tags: [String!]! customFields: [CustomField!]! """ Position of this event within its recurring series. Null if not part of a recurring series. """ recurringEventPosition: RecurringEventPosition forms: [EventForm!]! customerId: SafeInt customer: Customer createdByUserId: SafeInt! createdByUser: User lastUpdatedByUserId: SafeInt lastUpdatedByUser: User crewId: SafeInt crew: Crew color: String! completedByUserId: SafeInt completedByUser: User """ The category assigned to this event, resolved from categoryId. Null if none is set. """ category: EventCategory """Deprecated. Use users instead.""" employees: [EventEmployee!]! @deprecated(reason: "Use users") """ Users assigned to this event. Intentionally includes soft-deleted users — historical assignment records are preserved even after a user account is deactivated or deleted. To filter to active users only, pass where: { isDeleted: false }. """ users(where: UserFilter, orderBy: [UserSort!], take: SafeInt = 1000, skip: SafeInt): [User!]! dispatchNotes: [DispatchNote!] timeEntries(where: TimeEntryFilter, orderBy: [TimeEntrySort!], take: SafeInt = 1000, skip: SafeInt): [TimeEntry!]! timeEntryTotals: TimeEntryTotals! recurringEvent: RecurringEvent """Route stops for this event. Ordered by stopOrder ascending.""" routeStops(where: RouteStopFilter, orderBy: [RouteStopSort!], take: SafeInt = 1000, skip: SafeInt): [RouteStop!]! """ Finds the most efficient ways to insert this event's location into existing routes, grouped by day. By default, it evaluates the event's scheduled date range (startDate to endDate). Calculates the 'marginal cost' (extra time and distance) for each possible insertion point. Returns an empty list if the event or its property lacks valid coordinates. eventFilter applies as a "some" filter for routes and a direct filter for nearbyEvents. Set fastMode: true to skip directions API calls and use crow-flies throughout. """ routeSuggestions( """Override the start date. Defaults to the event's own startDate.""" startDate: LocalDate """ Override the end date. Defaults to the event's endDate (or startDate for single-day events). """ endDate: LocalDate """ Filter by crew, customer, or other event properties. Does not affect the date range. """ eventFilter: EventFilter """ If true, use crow-flies for all calculations. No directions API API calls made. """ fastMode: Boolean ): [SchedulingDateSuggestion!]! """ The most recent upsell history for this event, if any. Used to determine if an upsell has already been sent. """ latestUpsellHistory: UpsellHistory } input EventTypeFilter { equals: EventType not: EventType in: [EventType!] notIn: [EventType!] } """ Most searches and reporting should filter to OPEN, CLOSED and/or SKIPPED """ input EventStatusFilter { equals: EventStatus not: EventStatus in: [EventStatus!] notIn: [EventStatus!] } input WaitlistDueStatusFilter { equals: WaitlistDueStatus not: WaitlistDueStatus in: [WaitlistDueStatus!] notIn: [WaitlistDueStatus!] } input EventFilter { id: IntFilter type: EventTypeFilter title: StringFilter status: EventStatusFilter waitlistDueStatus: WaitlistDueStatusFilter propertyId: IntFilter categoryId: IntFilter estimateId: IntFilter startDate: DateFilter startDateTime: DateTimeFilter endDateTime: DateTimeFilter endDate: DateFilter budgetedHours: DecimalFilter lineItems: EventItemFilter users: UserFilter isDeleted: Boolean AND: [EventFilter!] OR: [EventFilter!] NOT: [EventFilter!] customerId: IntFilter customer: CustomerFilter crewId: IntFilter crew: CrewFilter property: PropertyFilter """Deprecated. Use users: UserFilter instead. Map employeeUserId → id.""" employees: EventEmployeeFilter @deprecated(reason: "Use users: UserFilter instead") dispatchNotes: StringFilter @deprecated(reason: "Broken — was silently ignored. Use hasDispatchNotes: Boolean instead.") """ Filter events by whether dispatch notes exist. true = has at least one note, false = no notes (null or empty). """ hasDispatchNotes: Boolean description: StringFilter invoiceId: IntFilter recurringEventId: IntFilter recurringId: IntFilter @deprecated(reason: "Use recurringEventId instead") """ Filter events by invoiced status. Pass true to find events linked to an invoice, false to find events with no invoice. """ isInvoiced: Boolean tags: TagsFilter """ Filter events that have at least one running timer (a time entry with no endedAt). Pass true to find events with active timers, false to find events with no active timers. """ hasRunningTimers: Boolean """ Filter events that have at least one time entry. Pass true to find events with any tracked time, false to find events with no time entries. """ hasTimeEntries: Boolean """ Filter events by recurring status. Pass true to find events that belong to a recurring series, false to find one-off events. """ isRecurring: Boolean """ Filter by last-write timestamp. Use for "changed since X" incremental sync. """ updatedAt: DateTimeFilter createdAt: DateTimeFilter """ When true, only events that have at least one RouteStop are returned. When false, only events with no RouteStops are returned. Omit to return all events regardless of route membership. """ isRouted: Boolean } input EventWhere { id: IntFilter type: EventTypeFilter @deprecated """Use EventFilter instead""" title: StringFilter status: EventStatusFilter waitlistDueStatus: WaitlistDueStatusFilter @deprecated """Use EventFilter instead""" propertyId: IntFilter @deprecated """Use EventFilter instead""" categoryId: IntFilter @deprecated """Use EventFilter instead""" estimateId: IntFilter @deprecated """Use EventFilter instead""" startDate: DateTimeFilter @deprecated """Use EventFilter instead""" startDateTime: DateTimeFilter @deprecated """Use EventFilter instead""" endDateTime: DateTimeFilter @deprecated """Use EventFilter instead""" endDate: DateTimeFilter @deprecated """Use EventFilter instead""" isDeleted: Boolean AND: [EventFilter!] @deprecated """Use EventFilter instead""" OR: [EventFilter!] @deprecated """Use EventFilter instead""" NOT: [EventFilter!] @deprecated } input EventItemFilter { id: IntFilter name: StringFilter itemId: IntFilter item: ItemFilter isDeleted: Boolean } input EventItemSort { id: SortOrder name: SortOrder } """ Sort options for Crew. Used standalone for crew queries and nested in EventSort. """ input CrewSort { id: SortOrder name: SortOrder } input EventSort { id: SortOrder title: SortOrder startDate: SortOrder startDateTime: SortOrder startTime: SortOrder endDate: SortOrder endDateTime: SortOrder sortOrder: SortOrder status: SortOrder budgetedHours: SortOrder type: SortOrder property: PropertySort recurringEvent: RecurringEventSort customer: CustomerSort """ Sort by number of time entries (asc = least tracked first, desc = most tracked first). Note: sorting by total tracked seconds is not possible via Prisma orderBy — only _count is supported on relations. """ timeEntriesCount: SortOrder """Sort by crew name. Null-crew events sort last.""" crew: CrewSort """Sort by location name. Null-location events sort last.""" location: LocationSort updatedAt: SortOrder createdAt: SortOrder } type BeforeAndAfter { id: SafeInt! beforeDescription: String afterDescription: String isClientVisible: Boolean! eventId: SafeInt! event: Event! beforeMedia: [Media!] afterMedia: [Media!] } input BeforeAndAfterSort { id: SortOrder afterDescription: SortOrder beforeDescription: SortOrder isClientVisible: SortOrder } input BeforeAndAfterWhere { id: IntFilter afterDescription: StringFilter beforeDescription: StringFilter isClientVisible: Boolean AND: [BeforeAndAfterWhere!] OR: [BeforeAndAfterWhere!] NOT: [BeforeAndAfterWhere!] } """Event line item""" type EventItem { id: SafeInt! eventId: SafeInt! itemId: SafeInt! name: String! description: String price: Decimal! quantity: Decimal! tax: Decimal total: Decimal! budgetedHours: Decimal isDeleted: Boolean! deletedAt: DateTime createdAt: DateTime! updatedAt: DateTime event: Event! """Shouldn't be nullable but the foreign key seems to be unreliable""" item: Item } """ CLOSED='Complete' to users. OPEN and CLOSED the billable, serviceable events. Most searches and reporting should filter to OPEN, CLOSED and/or SKIPPED """ enum EventStatus { """ Not yet scheduled, so the event's startDate and endDate are just a range for potential scheduling """ WAITLISTED """Active jobs ready to be completed""" OPEN """ Skipped and not serviced. This is usually due to weather, scheduling, staffing or equipment issue """ SKIPPED """AKA 'Complete' in the UI""" CLOSED """ This isn't a used, user-facing status and should be excluded. Currently the only way to cancel an event is to cancel its recurring series, which effectively deletes it """ CANCELLED """ Deprecated. Use isDeleted: true in EventFilter instead. Soft-deleted status is not part of event status. """ DELETED @deprecated(reason: "Use isDeleted: true in EventFilter instead") } enum EventType { """Default and most commonly used type for customer jobs""" VISIT TODO MEETING MAINTENANCE WORK_REQUEST } enum WaitlistDueStatus { UPCOMING DUE_SOON OVERDUE } input EventReportOrderBy { status: SortOrder type: SortOrder customerId: SortOrder crewId: SortOrder propertyId: SortOrder recurringEventId: SortOrder completedByUserId: SortOrder categoryId: SortOrder year: SortOrder month: SortOrder startDate: SortOrder _count: EventReportCountOrderBy _sum: EventReportSumOrderBy _avg: EventReportAvgOrderBy _min: EventReportMinOrderBy _max: EventReportMaxOrderBy count: EventReportCountOrderBy @deprecated(reason: "Use _count") sum: EventReportSumOrderBy @deprecated(reason: "Use _sum") avg: EventReportAvgOrderBy @deprecated(reason: "Use _avg") min: EventReportMinOrderBy @deprecated(reason: "Use _min") max: EventReportMaxOrderBy @deprecated(reason: "Use _max") } input EventReportCountOrderBy { id: SortOrder } input EventReportSumOrderBy { budgetedHours: SortOrder } input EventReportAvgOrderBy { budgetedHours: SortOrder } input EventReportMinOrderBy { startDate: SortOrder startDateTime: SortOrder budgetedHours: SortOrder } input EventReportMaxOrderBy { startDate: SortOrder startDateTime: SortOrder budgetedHours: SortOrder } type EventReportCountAggregate { _all: SafeInt! @deprecated(reason: "Use id instead") id: SafeInt! } type EventReportSumAggregate { budgetedHours: Decimal """ Sum of 'total' for all non-deleted line items on matching events. Only available when no group-by fields are selected — throws if group-by is present. """ total: Decimal """ Sum of 'totalSeconds' for completed time entries on matching events. Excludes in-progress running timers. Only available when no group-by fields are selected — throws if group-by is present. """ timeEntrySeconds: SafeInt } type EventReportAvgAggregate { budgetedHours: Decimal } type EventReportMinAggregate { startDate: Date startDateTime: LocalDateTime budgetedHours: Decimal } type EventReportMaxAggregate { startDate: Date startDateTime: LocalDateTime budgetedHours: Decimal } input EventLineItemInput { itemId: SafeInt! price: Decimal! quantity: Decimal! description: String tax: Decimal budgetedHours: Decimal } input UpdateEventLineItemInput { itemId: SafeInt price: Decimal quantity: Decimal description: String tax: Decimal budgetedHours: Decimal } enum RecurringFrequency { DAILY WEEKLY MONTHLY YEARLY } enum RecurringDailyOption { """Repeat every N days (N = dailyDaysCount)""" EVERY_N_DAYS """Repeat every weekday (Mon–Fri)""" EVERY_WEEKDAY } enum RecurringMonthlyOption { """Repeat on a specific day of the month (e.g. the 15th)""" ON_DAY_OF_MONTH """Repeat on a weekday ordinal (e.g. the second Tuesday)""" ON_WEEKDAY_OF_MONTH } enum RecurringYearlyOption { """Repeat on a specific month and day (e.g. March 15)""" ON_MONTH_DAY """ Repeat on a weekday ordinal within a month (e.g. second Tuesday of March) """ ON_MONTH_WEEKDAY } enum RecurringOrdinal { FIRST SECOND THIRD FOURTH LAST } enum RecurringWeekday { SUN MON TUE WED THU FRI SAT } enum RecurringMonth { JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC } type RecurringEvent { id: SafeInt! companyId: SafeInt! propertyId: SafeInt property: Property title: String! type: EventType! description: String! location: String! startDate: Date! """Use startDate instead""" dateStart: Date! @deprecated(reason: "Use startDate instead") endDate: Date! startDateTime: DateTime! endDateTime: DateTime """Whether this recurring event has a specific time of day vs. all-day""" hasTime: Boolean! startTime: LocalTime! endTime: LocalTime! frequency: RecurringFrequency! dailyOption: RecurringDailyOption! dailyDaysCount: Int! weeklyCount: Int! """Monday selected""" weekday1: Boolean! """Tuesday selected""" weekday2: Boolean! """Wednesday selected""" weekday3: Boolean! """Thursday selected""" weekday4: Boolean! """Friday selected""" weekday5: Boolean! """Saturday selected""" weekday6: Boolean! """Sunday selected""" weekday7: Boolean! """Only relevant when frequency is MONTHLY""" monthlyOption: RecurringMonthlyOption monthDay: Int! monthCount1: Int! monthDayNumber: RecurringOrdinal monthWeekDay: RecurringWeekday monthCount2: Int! """Only relevant when frequency is YEARLY""" yearlyOption: RecurringYearlyOption yearMonth1: RecurringMonth yearDay: Int! yearDayNumber: RecurringOrdinal yearWeekDay: RecurringWeekday yearMonth2: RecurringMonth isInfinite: Boolean! isCancelled: Boolean! """ Timestamp when this recurring series was cancelled. Stored in the company's local timezone by the PHP layer; exposed here as UTC. Null if the series has never been cancelled (i.e. isCancelled is false). """ cancelledAt: DateTime maintenanceCost: Decimal! maintenanceTax1: Decimal! maintenanceTax1Amount: Decimal! maintenanceTax2: Decimal! maintenanceTax2Amount: Decimal! isInvoiceable: Boolean! budgetedHours: Decimal! equipmentId: SafeInt estimateId: SafeInt """ID of the linked work request, if any.""" workRequestId: SafeInt requestId: SafeInt @deprecated(reason: "Use workRequestId instead") contractId: SafeInt isChemicalApplication: Boolean! lineItems(where: RecurringEventItemFilter, orderBy: [RecurringEventItemSort!], take: SafeInt = 1000, skip: SafeInt): [RecurringEventItem!]! events(where: EventFilter, orderBy: [EventSort!], take: SafeInt = 1000, skip: SafeInt): [Event!]! """Number of non-deleted events in this recurring series.""" eventCount: Int! """Use eventCount instead""" eventsCount: Int! @deprecated(reason: "Use eventCount instead") tags: [String!]! """ID of the off-season assigned to this series, if any.""" offSeasonId: SafeInt """The off-season assigned to this series, if any.""" offSeason: OffSeason """ Derived status of this recurring series. CANCELLED — isCancelled is true. PAUSED — an off-season is assigned and today falls within its date range. ACTIVE — all other cases. """ status: RecurringSeriesStatus! customerId: SafeInt customer: Customer crewId: SafeInt crew: Crew color: String! """ Users assigned to this recurring event series. Intentionally includes soft-deleted users — historical assignment records are preserved even after a user account is deactivated or deleted. To filter to active users only, pass where: { isDeleted: false }. """ users(where: UserFilter, take: SafeInt = 1000, skip: SafeInt): [User!]! } input RecurringEventItemFilter { id: IntFilter name: StringFilter } input RecurringEventItemSort { id: SortOrder name: SortOrder } type RecurringEventItem { id: SafeInt! companyId: SafeInt! recurringEventId: SafeInt! itemId: SafeInt item: Item name: String description: String price: Decimal! tax: Decimal total: Decimal quantity: Decimal! } input RecurringEventFilter { id: IntFilter companyId: IntFilter propertyId: IntFilter isCancelled: Boolean isInfinite: Boolean startDateTime: DateTimeFilter endDateTime: DateTimeFilter startDate: DateFilter """Use startDate instead""" dateStart: DateFilter @deprecated(reason: "Use startDate instead") endDate: DateFilter title: StringFilter type: EventTypeFilter estimateId: IntFilter tags: TagsFilter AND: [RecurringEventFilter!] OR: [RecurringEventFilter!] NOT: RecurringEventFilter customerId: IntFilter crewId: IntFilter } input RecurringEventSort { id: SortOrder startDateTime: SortOrder endDateTime: SortOrder startDate: SortOrder """Use startDate instead""" dateStart: SortOrder @deprecated(reason: "Use startDate instead") endDate: SortOrder title: SortOrder startTime: SortOrder frequency: SortOrder customer: CustomerSort propertyId: SortOrder customerId: SortOrder } interface FileInterface { size: SafeInt contentType: String thumb(height: SafeInt = 1200, width: SafeInt = 1200, quality: SafeInt): String url: String! } type File implements FileInterface { size: SafeInt contentType: String thumb(height: SafeInt = 1200, width: SafeInt = 1200, quality: SafeInt): String url: String! } input InvoiceReportOrderBy { status: SortOrder customerId: SortOrder propertyId: SortOrder date: SortOrder isSent: SortOrder source: SortOrder _count: InvoiceReportCountOrderBy _sum: InvoiceReportSumOrderBy _avg: InvoiceReportAvgOrderBy _min: InvoiceReportMinOrderBy _max: InvoiceReportMaxOrderBy count: InvoiceReportCountOrderBy @deprecated(reason: "Use _count") sum: InvoiceReportSumOrderBy @deprecated(reason: "Use _sum") avg: InvoiceReportAvgOrderBy @deprecated(reason: "Use _avg") min: InvoiceReportMinOrderBy @deprecated(reason: "Use _min") max: InvoiceReportMaxOrderBy @deprecated(reason: "Use _max") } input InvoiceReportCountOrderBy { id: SortOrder } input InvoiceReportSumOrderBy { subtotal: SortOrder total: SortOrder tax: SortOrder discountAmount: SortOrder paidAmount: SortOrder lateFee: SortOrder processingFee: SortOrder } input InvoiceReportAvgOrderBy { subtotal: SortOrder total: SortOrder tax: SortOrder discountAmount: SortOrder paidAmount: SortOrder lateFee: SortOrder processingFee: SortOrder } input InvoiceReportMinOrderBy { date: SortOrder subtotal: SortOrder total: SortOrder tax: SortOrder discountAmount: SortOrder paidAmount: SortOrder lateFee: SortOrder processingFee: SortOrder } input InvoiceReportMaxOrderBy { date: SortOrder subtotal: SortOrder total: SortOrder tax: SortOrder discountAmount: SortOrder paidAmount: SortOrder lateFee: SortOrder processingFee: SortOrder } type InvoiceReportCountAggregate { id: SafeInt! } type InvoiceReportSumAggregate { subtotal: Decimal total: Decimal tax: Decimal discountAmount: Decimal paidAmount: Decimal lateFee: Decimal processingFee: Decimal } type InvoiceReportAvgAggregate { subtotal: Decimal total: Decimal tax: Decimal discountAmount: Decimal paidAmount: Decimal lateFee: Decimal processingFee: Decimal } type InvoiceReportMinAggregate { date: Date subtotal: Decimal total: Decimal tax: Decimal discountAmount: Decimal paidAmount: Decimal lateFee: Decimal processingFee: Decimal } type InvoiceReportMaxAggregate { date: Date subtotal: Decimal total: Decimal tax: Decimal discountAmount: Decimal paidAmount: Decimal lateFee: Decimal processingFee: Decimal } enum InvoiceDeletedState { """Invoice is active""" ACTIVE """Invoice has been soft-deleted""" DELETED """Invoice was deleted as part of a cascade (e.g. parent record deleted)""" CASCADE_DELETED } enum InvoiceStatus { """Created but not yet sent to the customer""" DRAFT """Sent to the customer, awaiting payment""" PENDING """Payment received in full""" PAID """A portion has been paid, balance remains""" PARTIALLY_PAID """Payment overdue past the invoice due date""" PAST_DUE """ Written off — debt forgiven or uncollectable (shown as 'Written Off' in UI) """ WRITE_OFF } enum InvoiceHistoryAction { ARCHIVE CANCEL COMBINED CREATE DROPPED_OFF EMAIL EMAIL_OPEN LINK_COPY MAIL MARK_DRAFT MARK_EMAIL MARK_MAIL MARK_NOTSEND MARK_WRITTENOFF PAYMENT PROCESSING_FEE REFUND SMS UNARCHIVE UPDATE VIEW STRIPE_PAYMENT_FAILED } type Invoice { id: SafeInt! number: SafeInt purchaseOrder: String! date: Date! status: InvoiceStatus! title: String notes: String! terms: String! subtotal: Decimal! discountPercent: Decimal! discountAmount: Decimal! tax: Decimal! total: Decimal! paidAmount: Decimal! isSent: Boolean! hasBeenViewed: Boolean! isArchived: Boolean! contract: SafeInt deletedState: InvoiceDeletedState! lateFee: Decimal! processingFee: Decimal! propertyId: SafeInt """Timestamp of the last write to this invoice record.""" updatedAt: DateTime """@deprecated Use updatedAt instead.""" lastUpdated: DateTime @deprecated(reason: "Use updatedAt") lastAutoChargeAttempt: DateTime autoChargedAt: DateTime sentAt: DateTime sorted: Boolean! lateFeeRule: SafeInt! lateFeeRuleVersion: SafeInt! createdAt: DateTime """ The number of days this invoice is past due, considering company and customer late fee rules """ daysPastDue: Int! """The due date for this invoice, calculated based on late fee rules""" dueDate: DateTime media(where: MediaWhere, orderBy: [MediaSort!], take: SafeInt = 1000, skip: SafeInt): [Media!]! lineItems(where: InvoiceLineItemFilter, orderBy: [InvoiceLineItemSort!], take: SafeInt = 1000, skip: SafeInt): [InvoiceLineItem!]! history(where: InvoiceHistoryFilter, orderBy: [InvoiceHistorySort!], take: SafeInt = 1000, skip: SafeInt): [InvoiceHistory!]! customFields: [CustomField!]! payments(where: PaymentFilter = {isDeleted: false}, orderBy: [PaymentSort!], take: SafeInt = 1000, skip: SafeInt): [Payment!]! customerId: SafeInt! internalNotes: String sentEmail: Boolean! sentSnailMail: Boolean! droppedOffAtProperty: Boolean! tags: String! emailSubject: String! sentSms: Boolean! emailMessage: String! quickbooksLastSynced: DateTime quickbooksId: SafeInt quickbooksSyncStatus: QuickbooksSyncStatus quickbooksMeta: String quickbooksSyncFailedCount: SafeInt! quickbooksSyncFailedMessage: String isImportedFromQuickbooks: Boolean! autoSendReadyAt: DateTime autoSentAt: DateTime emailOpen: Boolean! source: String sentCopyLink: Boolean! """ Source estimate that created this invoice, if any. Resolved via Estimate.invoiceId (no estimateId column on invoices). Null when the invoice was not created from an estimate. If multiple non-deleted estimates share the same invoiceId, the lowest estimate id is returned. """ estimateId: SafeInt """Source estimate for this invoice. Same resolution rules as estimateId.""" estimate: Estimate } type InvoiceLineItem { id: SafeInt! eventId: SafeInt eventServiceId: SafeInt eventIsRecurring: Boolean! date: Date itemId: SafeInt name: String! description: String! price: Decimal! cost: Decimal! @deprecated(reason: "Use price instead") quantity: Decimal! budgetedHours: Decimal taxPercent1: Decimal! taxPercent2: Decimal! taxPrice1: Decimal! taxPrice2: Decimal! subtotal: Decimal! subtotalWithTax: Decimal! subtotalWithDiscount: Decimal! propertyId: SafeInt property: Property isRecommended: Boolean! """Rename to isRemoved?""" removed: EstimateLineItemRemoved! lineItemOrder: SafeInt! } type InvoiceHistory { id: SafeInt! date: DateTime action: InvoiceHistoryAction actionReason: String comment: String userId: SafeInt customerId: SafeInt user: InvoiceHistoryUser customer: InvoiceHistoryUser meta: String payment: Payment invoice: Invoice } type InvoiceHistoryUser { id: SafeInt! fullName: String } input InvoiceLineItemFilter { id: IntFilter name: StringFilter } input InvoiceLineItemSort { id: SortOrder name: SortOrder lineItemOrder: SortOrder } input InvoiceHistoryFilter { id: IntFilter action: StringFilter } input InvoiceHistorySort { id: SortOrder date: SortOrder } input InvoiceStatusFilter { equals: InvoiceStatus not: InvoiceStatus in: [InvoiceStatus!] notIn: [InvoiceStatus!] } input InvoiceDeletedStateFilter { equals: InvoiceDeletedState not: InvoiceDeletedState in: [InvoiceDeletedState!] notIn: [InvoiceDeletedState!] } input InvoiceFilter { id: IntFilter number: IntFilter purchaseOrder: StringFilter date: DateFilter status: InvoiceStatusFilter title: StringFilter isArchived: Boolean deletedState: InvoiceDeletedStateFilter """ Filter by last-write timestamp. Use for "changed since X" incremental sync. """ updatedAt: DateTimeFilter createdAt: DateTimeFilter customerId: IntFilter } input InvoiceSort { id: SortOrder number: SortOrder purchaseOrder: SortOrder companyId: SortOrder customerId: SortOrder date: SortOrder status: SortOrder title: SortOrder sent: SortOrder updatedAt: SortOrder createdAt: SortOrder } enum ItemMarkupType { """No markup — price is the flat item price""" NONE """Percentage markup: item_markup is a percentage added on top of cost""" PERCENTAGE """ Flat dollar markup: item_markup is a fixed dollar amount added on top of cost """ FLAT } enum ItemType { SERVICE ITEM LABOR OTHER } type ItemCategory { id: SafeInt! name: String! description: String! isRoot: Boolean! items(where: ItemFilter = {isDeleted: false}, orderBy: [ItemSort!], take: SafeInt = 1000, skip: SafeInt): [Item!]! } input ItemCategoryFilter { id: IntFilter } input ItemCategorySort { id: SortOrder name: SortOrder } input ItemFilter { category: ItemCategoryFilter events: EventFilter id: IntFilter isDeleted: Boolean name: StringFilter description: StringFilter price: DecimalFilter rateChargedP4p: DecimalFilter inventoryOnHand: IntFilter wholesaleCost: DecimalFilter markup: DecimalFilter vendor: VendorFilter AND: [ItemFilter!] OR: [ItemFilter!] NOT: ItemFilter } type Item { id: SafeInt! categoryId: SafeInt! category: ItemCategory type: ItemType! name: String! description: String! price: Decimal! quantity: Decimal! budgetedHours: Decimal tax1: Decimal! tax2: Decimal! unit: String! supplier: String! supplierId: SafeInt! materialCost: Decimal! equipmentCost: Decimal! subcontractCost: Decimal! isDeleted: Boolean! hasInventory: Boolean! isGlobal: Boolean! isTaxable: Boolean! color: String vendor: Vendor eventCount(where: EventFilter): SafeInt! wholesaleCost: Decimal! inventoryOnHand: SafeInt! markup: Decimal! markupType: ItemMarkupType! rateChargedP4p: Decimal! } input ItemSort { id: SortOrder name: SortOrder categoryId: SortOrder category: ItemCategorySort } type ItemPackage { id: Int! name: String description: String! categoryId: Int! itemIds: [SafeInt!]! items: [Item!]! isDeleted: Boolean! createdAt: DateTime! } input ItemPackageFilter { id: IntFilter name: StringFilter categoryId: IntFilter isDeleted: Boolean AND: [ItemPackageFilter!] OR: [ItemPackageFilter!] NOT: ItemPackageFilter } input ItemPackageSort { id: SortOrder name: SortOrder createdAt: SortOrder } type Location { name: String latitude: Float! longitude: Float! } input LocationInput { """Address or place name.""" name: String latitude: Float! longitude: Float! } input LocationSort { name: SortOrder } enum MediaModule { TEMPORARY BEFORE_AFTER CUSTOMER INVOICE VISIT ASSET EQUIPMENT ESTIMATE EXPENSE RECURRING VENDOR CHEMICAL CHEMICAL_APP } type Media implements FileInterface { id: SafeInt! companyId: SafeInt! module: MediaModule! objectId: SafeInt! fileName: String! size: SafeInt contentType: String thumb(height: SafeInt = 1200, width: SafeInt = 1200, quality: SafeInt): String url: String! description: String! uploadedAt: DateTime movedToFolderAt: DateTime } input MediaSort { fileName: SortOrder size: SortOrder contentType: SortOrder uploadedAt: SortOrder } input MediaWhere { fileName: StringFilter size: IntFilter description: StringFilter uploadedAt: DateTimeFilter } enum OrderGateway { AUTHORIZE STRIPE } enum OrderPaymentType { EMPTY PAYPAL VISA MASTERCARD AMEX DISCOVER UNKNOWN } type Order { id: SafeInt! date: DateTime! companyId: SafeInt userId: SafeInt! status: String! productId: SafeInt! productSku: String! """ Plan/package derived from productSku when it matches a known Stripe price """ plan: Plan productName: String! quantity: SafeInt! amount: Decimal! paymentType: OrderPaymentType! transactionId: String! last4: String! isRecurring: Boolean! isStoreOrder: Boolean! gateway: OrderGateway! stripeInvoiceId: String! requiresShipping: Boolean! receiptUrl: String } input OrderFilter { id: IntFilter companyId: IntFilter date: DateTimeFilter status: StringFilter productSku: StringFilter productName: StringFilter amount: DecimalFilter transactionId: StringFilter requiresShipping: Boolean AND: [OrderFilter!] OR: [OrderFilter!] NOT: [OrderFilter!] } input OrderSort { id: SortOrder date: SortOrder status: SortOrder amount: SortOrder companyId: SortOrder productSku: SortOrder productName: SortOrder } input PaymentReportOrderBy { date: SortOrder method: SortOrder customerId: SortOrder isRefund: SortOrder _count: PaymentReportCountOrderBy _sum: PaymentReportSumOrderBy _avg: PaymentReportAvgOrderBy _min: PaymentReportMinOrderBy _max: PaymentReportMaxOrderBy count: PaymentReportCountOrderBy @deprecated(reason: "Use _count") sum: PaymentReportSumOrderBy @deprecated(reason: "Use _sum") avg: PaymentReportAvgOrderBy @deprecated(reason: "Use _avg") min: PaymentReportMinOrderBy @deprecated(reason: "Use _min") max: PaymentReportMaxOrderBy @deprecated(reason: "Use _max") } input PaymentReportCountOrderBy { id: SortOrder } input PaymentReportSumOrderBy { totalAmount: SortOrder invoiceAmount: SortOrder creditAmount: SortOrder tipAmount: SortOrder processFeeCharged: SortOrder } input PaymentReportAvgOrderBy { totalAmount: SortOrder invoiceAmount: SortOrder creditAmount: SortOrder tipAmount: SortOrder processFeeCharged: SortOrder } input PaymentReportMinOrderBy { date: SortOrder totalAmount: SortOrder invoiceAmount: SortOrder creditAmount: SortOrder tipAmount: SortOrder processFeeCharged: SortOrder } input PaymentReportMaxOrderBy { date: SortOrder totalAmount: SortOrder invoiceAmount: SortOrder creditAmount: SortOrder tipAmount: SortOrder processFeeCharged: SortOrder } type PaymentReportCountAggregate { id: SafeInt! } type PaymentReportSumAggregate { totalAmount: Decimal invoiceAmount: Decimal creditAmount: Decimal tipAmount: Decimal processFeeCharged: Decimal } type PaymentReportAvgAggregate { totalAmount: Decimal invoiceAmount: Decimal creditAmount: Decimal tipAmount: Decimal processFeeCharged: Decimal } type PaymentReportMinAggregate { date: Date totalAmount: Decimal invoiceAmount: Decimal creditAmount: Decimal tipAmount: Decimal processFeeCharged: Decimal } type PaymentReportMaxAggregate { date: Date totalAmount: Decimal invoiceAmount: Decimal creditAmount: Decimal tipAmount: Decimal processFeeCharged: Decimal } enum PaymentMethod { CASH CHECK CREDIT_CARD CUSTOMER_CREDIT_BALANCE ACH_TRANSFER PAYPAL OTHER DEBIT_CARD DIRECT_DEBIT BACS VENMO WISETACK ZELLE CASH_APP APPLE_PAY EFT } type Payment { id: SafeInt! customerId: SafeInt customer: Customer method: PaymentMethod methodDisplayName: String date: Date! """Stores the invoice/payment amount without processing fee""" totalAmount: Decimal! invoiceAmount: Decimal! invoiceId: SafeInt creditAmount: Decimal! notes: String! details: String isRefund: Boolean! refundedAt: DateTime tipAmount: Decimal! processFeeCharged: Decimal! processFeeChargedTo: PaymentProcessFeePayer """Represents the actual amount the customer paid (including fee)""" paidAmount: Decimal! createdAt: DateTime updatedAt: DateTime invoice: Invoice userId: SafeInt expenseId: SafeInt """Resolved payee for expense payments (null for non-expense payments).""" payee: PaymentPayee } enum PaymentProcessFeePayer { CUSTOMER } input PaymentSort { id: SortOrder method: SortOrder date: SortOrder totalAmount: SortOrder invoiceAmount: SortOrder creditAmount: SortOrder refundedAt: SortOrder tipAmount: SortOrder processFeeCharged: SortOrder createdAt: SortOrder updatedAt: SortOrder } input PaymentMethodFilter { equals: PaymentMethod not: PaymentMethod in: [PaymentMethod!] notIn: [PaymentMethod!] } input PaymentFilter { id: IntFilter method: PaymentMethodFilter date: DateFilter totalAmount: DecimalFilter invoiceAmount: DecimalFilter creditAmount: DecimalFilter notes: StringFilter details: StringFilter isRefund: Boolean isDeleted: Boolean refundedAt: DateTimeFilter tipAmount: DecimalFilter processFeeCharged: DecimalFilter customerId: IntFilter invoiceId: IntFilter expenseId: IntFilter """ Filter by last-write timestamp. Use for "changed since X" incremental sync. """ updatedAt: DateTimeFilter createdAt: DateTimeFilter isExpensePayment: Boolean hasTip: Boolean } enum SubscriptionTier { """Legacy feature set""" ENTERPRISE_LEGACY """Legacy feature set""" PRO STARTER SOLO GROWTH ENTERPRISE } enum BillingInterval { MONTHLY YEARLY } type Plan { sku: String! name: String! description: String! price: Decimal! tier: Tier! billingInterval: BillingInterval! """Is part of current pricing. If false, it's a legacy plan""" isCurrent: Boolean! isFreeTrial: Boolean! isAddon: Boolean! } type Tier { id: SubscriptionTier! level: Int! name: String! features: TierFeatures! """Price per additional SMS segment beyond the included allowance""" smsPrice: Decimal! """Price per additional user seat beyond the included allowance""" additionalUserSeatPrice: Decimal! } type TierFeatures { generalContactLimit: Int! hasCommunicationAccess: Boolean! hasApplicationFee: Boolean! hasCustomerPortal: Boolean! hasSimpleAutomations: Boolean! hasUpsells: Boolean! hasEmailTemplates: Boolean! hasEmailLinks: Boolean! hasIntercom: Boolean! hasTipping: Boolean! hasBackups: Boolean! hasProTimer: Boolean! hasRouteOptimization: Boolean! hasChemicalApplication: Boolean! hasPaymentMethodRequired: Boolean! hasSMS: Boolean! hasQuickbooksSync: Boolean! hasPriceMatrix: Boolean! hasAutoCharge: Boolean! hasACHPayments: Boolean! hasAdvancedAutomations: Boolean! hasPassThroughFees: Boolean! hasPayInFull: Boolean! hasWorkRequests: Boolean! hasEstimateDeposits: Boolean! hasCallNotes: Boolean! hasWaitlist: Boolean! includedUsers: Int! includedSmsSegments: Int! hasApiAccess: Boolean! hasApiTrial: Boolean! workRequests: Boolean @deprecated payInFull: Boolean @deprecated tipping: Boolean @deprecated } """Deletion/archive status of a property.""" enum PropertyDeletedStatus { """Property is active and in use.""" ACTIVE """Property has been explicitly deleted.""" DELETED """ Property is hidden because its parent customer was deactivated. Restored automatically when the customer is reactivated. """ CUSTOMER_DEACTIVATED } """ Unit of measurement for property area fields (size, pavedSize, houseSize, etc.). """ enum AreaMeasurement { """Square feet (default)""" SQ_FEET """Square meters""" SQ_METERS """Acres""" ACRES """Hectares""" HECTARES } type PropertyCrew { id: SafeInt! crewId: SafeInt! companyId: SafeInt! assignedAt: DateTime status: SafeInt! notes: String } type Property { id: SafeInt! requestId: SafeInt name: String! address: Address """ The property's address, geocoded as a coordinate pair. Null if not yet geocoded. """ location: Location size: Decimal! sizeType: AreaMeasurement! pavedSize: Decimal! pavedSizeType: AreaMeasurement! houseSize: Decimal! houseSizeType: AreaMeasurement! plantingSize: Decimal! plantingSizeType: AreaMeasurement! roofAreaSize: Decimal! roofAreaSizeType: AreaMeasurement! roofPerimeterSize: Decimal! roofPerimeterSizeType: AreaMeasurement! drivewayWalkwaySize: Decimal! drivewayWalkwaySizeType: AreaMeasurement! roofPitch: SafeInt fence: String storiesTall: SafeInt notes: String! """ Deletion/archive status. Use ACTIVE for normal properties. DELETED means explicitly removed; CUSTOMER_DEACTIVATED means the parent customer was deactivated (auto-restored on reactivation). """ deletedStatus: PropertyDeletedStatus! isActive: Boolean! tags: String! tax1: Decimal! tax2: Decimal! tax3: Decimal! taxTotal: Decimal! createdAt: DateTime updatedAt: DateTime media(where: MediaWhere, orderBy: [MediaSort!], take: SafeInt = 1000, skip: SafeInt): [Media!]! customer: Customer customerId: SafeInt! lastServiceDate: DateTime propertyCrews: [PropertyCrew!]! """ Custom field values for this property (objectType = ASSET). CRM-only — not exposed on the customer portal, where property custom fields (e.g. Gate Code) must never reach clients. """ customFields: [CustomField!]! } input PropertyFilter { id: IntFilter """Matches the property's address fields (street1, city, zip, etc.).""" address: AddressFilter name: StringFilter deletedStatus: PropertyDeletedStatus isActive: Boolean customerId: IntFilter customer: CustomerFilter size: DecimalFilter } """ Input for creating a property. `customerId` and `name` are required; all other fields are optional. """ input PropertyInput { """The customer this property belongs to.""" customerId: SafeInt! """Display name for the property (e.g. "123 Main St Property").""" name: String! address: AddressInput notes: String """Unit of measurement for all size fields. Defaults to SQ_FEET.""" sizeType: AreaMeasurement """ Tag names to attach. Tags are created if they don't already exist for this company. """ tags: [String!] """ Custom field values to set on the property at creation time. Each fieldId must reference a DynamicCustomField with objectType = ASSET for this company. """ customFields: [CustomFieldInput!] """Crews to assign. Each must belong to this company.""" crewIds: [SafeInt!] """ S3 temp file keys to attach as media. Obtain keys via the createUpload mutation: call createUpload, PUT the file to the returned uploadUrl, then pass the key here. """ mediaFileKeys: [String!] } """ Input for updating a property. All fields are optional — only provided fields are written. """ input UpdatePropertyInput { """Display name for the property.""" name: String notes: String address: AddressInput """Unit of measurement for all size fields.""" sizeType: AreaMeasurement size: Decimal pavedSize: Decimal houseSize: Decimal plantingSize: Decimal roofAreaSize: Decimal roofPerimeterSize: Decimal drivewayWalkwaySize: Decimal """ Crew IDs to assign. Replace-set semantics — supplying this field replaces the current crew set; omitting it leaves crews untouched. """ crewIds: [SafeInt!] """ Tag names to attach. Replace-set semantics — supplying this field replaces the current tag set; omitting it leaves tags untouched. """ tags: [String!] """ Custom field values to set. Replace-set semantics — supplying this field upserts by fieldId; omitting it leaves custom fields untouched. """ customFields: [CustomFieldInput!] } input PropertySort { id: SortOrder name: SortOrder address: AddressSort createdAt: SortOrder updatedAt: SortOrder } type Rating { id: SafeInt! value: Float! note: String! createdAt: DateTime! serviceName: String! } input SmsReportCountOrderBy { id: SortOrder } input SmsReportSumOrderBy { segments: SortOrder } input SmsReportAvgOrderBy { segments: SortOrder } input SmsReportMinOrderBy { segments: SortOrder createdAt: SortOrder sentAt: SortOrder } input SmsReportMaxOrderBy { segments: SortOrder createdAt: SortOrder sentAt: SortOrder } input SmsReportOrderBy { direction: SortOrder status: SortOrder stripeStatus: SortOrder customerId: SortOrder counterPartyUserId: SortOrder sentByUserId: SortOrder conversationId: SortOrder isViewed: SortOrder isDone: SortOrder _count: SmsReportCountOrderBy _sum: SmsReportSumOrderBy _avg: SmsReportAvgOrderBy _min: SmsReportMinOrderBy _max: SmsReportMaxOrderBy count: SmsReportCountOrderBy @deprecated(reason: "Use _count") sum: SmsReportSumOrderBy @deprecated(reason: "Use _sum") avg: SmsReportAvgOrderBy @deprecated(reason: "Use _avg") min: SmsReportMinOrderBy @deprecated(reason: "Use _min") max: SmsReportMaxOrderBy @deprecated(reason: "Use _max") } type SmsReportCountAggregate { id: SafeInt! } type SmsReportSumAggregate { segments: SafeInt } type SmsReportAvgAggregate { segments: Float } type SmsReportMinAggregate { segments: SafeInt createdAt: DateTime sentAt: DateTime } type SmsReportMaxAggregate { segments: SafeInt createdAt: DateTime sentAt: DateTime } type SmsReportRow { direction: SmsDirection status: SmsStatus stripeStatus: SmsStripeStatus customerId: SafeInt """ The user/employee on the other end of this conversation (sms_employee_id) """ counterPartyUserId: SafeInt """The Homeworks user who sent the outbound message (sms_user_id)""" sentByUserId: SafeInt conversationId: String isViewed: Boolean isDone: Boolean customer: Customer counterPartyUser: User sentByUser: User _count: SmsReportCountAggregate _sum: SmsReportSumAggregate _avg: SmsReportAvgAggregate _min: SmsReportMinAggregate _max: SmsReportMaxAggregate count: SmsReportCountAggregate @deprecated(reason: "Use _count") sum: SmsReportSumAggregate @deprecated(reason: "Use _sum") avg: SmsReportAvgAggregate @deprecated(reason: "Use _avg") min: SmsReportMinAggregate @deprecated(reason: "Use _min") max: SmsReportMaxAggregate @deprecated(reason: "Use _max") } enum SmsStatus { SENT DELIVERED FAILED QUEUED SENDING UNDELIVERED RECEIVING RECEIVED ACCEPTED SCHEDULED READ PARTIALLY_DELIVERED CANCELED } enum SmsDirection { IN OUT } enum SmsStripeStatus { EXEMPT PENDING POSTED } input SmsStatusFilter { equals: SmsStatus not: SmsStatus in: [SmsStatus!] notIn: [SmsStatus!] } input SmsDirectionFilter { equals: SmsDirection not: SmsDirection in: [SmsDirection!] notIn: [SmsDirection!] } input SmsStripeStatusFilter { equals: SmsStripeStatus not: SmsStripeStatus in: [SmsStripeStatus!] notIn: [SmsStripeStatus!] } enum StripeWalletType { apple_pay google_pay cash_app_pay samsung_pay link masterpass visa_checkout amex_express_checkout } enum StripePaymentMethodType { billie card crypto acss_debit affirm afterpay_clearpay alipay alma amazon_pay au_becs_debit bacs_debit bancontact blik boleto card_present cashapp customer_balance eps fpx giropay grabpay ideal interac_present kakao_pay klarna konbini kr_card link mobilepay multibanco naver_pay nz_bank_account oxxo p24 pay_by_bank payco paynow paypal pix promptpay revolut_pay samsung_pay satispay sepa_debit sofort swish twint us_bank_account wechat_pay zip } enum StripePaymentMethodAllowRedisplay { always limited unspecified } type StripePaymentMethod { id: String! object: String! type: StripePaymentMethodType! created: DateTime! allow_redisplay: StripePaymentMethodAllowRedisplay livemode: Boolean! metadata: JSON billing_details: JSON! status: StripePaymentMethodStatus verificationMethod: StripeVerificationMethod card: JSON acss_debit: JSON affirm: JSON afterpay_clearpay: JSON alipay: JSON alma: JSON amazon_pay: JSON au_becs_debit: JSON bacs_debit: JSON bancontact: JSON blik: JSON boleto: JSON card_present: JSON cashapp: JSON customer_balance: JSON eps: JSON fpx: JSON giropay: JSON grabpay: JSON ideal: JSON interac_present: JSON kakao_pay: JSON klarna: JSON konbini: JSON kr_card: JSON link: JSON mobilepay: JSON multibanco: JSON naver_pay: JSON oxxo: JSON p24: JSON pay_by_bank: JSON payco: JSON paynow: JSON paypal: JSON pix: JSON promptpay: JSON revolut_pay: JSON samsung_pay: JSON sepa_debit: JSON sofort: JSON swish: JSON twint: JSON us_bank_account: JSON wechat_pay: JSON zip: JSON } enum StripePaymentMethodStatus { VERIFIED UNVERIFIED PENDING_VERIFICATION } enum StripeVerificationMethod { amounts descriptor_code } enum StripePaymentSourceType { bank_account card } union StripePaymentSource = StripeCard | StripeBankAccount """A card object from Stripe.""" type StripeCard { """Unique identifier for the object.""" id: String! """String representing the object's type. Should always be "card".""" object: StripePaymentSourceType! """Indicates if this card can be redisplayed in a checkout flow.""" allow_redisplay: String """Available payout methods for this card.""" available_payout_methods: [String] """Card brand.""" brand: String! """Two-letter ISO code representing the card's country.""" country: String """Currency supported by the card (if applicable).""" currency: Currency """The customer this card belongs to.""" customer: String """Result of the CVC check: pass, fail, unavailable, or unchecked.""" cvc_check: String """Whether this is the default external account for its currency.""" default_for_currency: Boolean """Always true for a deleted object.""" deleted: Boolean """Internal description of card type.""" description: String """Last 4 digits of device account number (tokenized cards).""" dynamic_last4: String """Expiration month.""" exp_month: Int! """Expiration year.""" exp_year: Int! """Card fingerprint for identifying duplicate cards.""" fingerprint: String """Funding type: credit, debit, prepaid, or unknown.""" funding: String! """Issuer identification number.""" iin: String """Name of the issuing bank.""" issuer: String """Last 4 digits of the card.""" last4: String! """Key-value metadata attached to the card.""" metadata: JSON """Cardholder name.""" name: String """Status based on the card issuer.""" regulated_status: String """Status of the card: new, errored, etc.""" status: String """Method used to tokenize the card.""" tokenization_method: String } """A bank account object from Stripe.""" type StripeBankAccount { """Unique identifier for the object.""" id: String! """ String representing the object's type. Should always be "bank_account". """ object: StripePaymentSourceType! """The name of the person or business that owns the bank account.""" account_holder_name: String """ The type of entity that holds the account. This can be either `individual` or `company`. """ account_holder_type: StripeBankAccountHolderType """ The bank account type. This can only be `checking` or `savings` in most countries. In Japan, this can only be `futsu` or `toza`. """ account_type: String """A set of available payout methods for this bank account.""" available_payout_methods: [String] """ Name of the bank associated with the routing number (e.g., `WELLS FARGO`). """ bank_name: String """ Two-letter ISO code representing the country the bank account is located in. """ country: String """Three-letter ISO code for the currency paid out to the bank account.""" currency: Currency """The ID of the customer that the bank account is associated with.""" customer: String """ Whether this bank account is the default external account for its currency. """ default_for_currency: Boolean """Always true for a deleted object""" deleted: Boolean """Uniquely identifies this particular bank account.""" fingerprint: String """The last four digits of the bank account number.""" last4: String! """Key-value metadata that can be attached to the object.""" metadata: JSON """The routing transit number for the bank account.""" routing_number: String """The verification status of the bank account.""" status: StripeBankAccountStatus! } enum StripeBankAccountStatus { new validated verified verification_failed errored } enum StripeBankAccountHolderType { individual company } type StripeSourceObject { id: ID! object: String! type: String usage: String status: String } enum StripeIntentType { PAYMENT SETUP } enum StripePaymentIntentStatus { canceled processing requires_action requires_capture requires_confirmation requires_payment_method succeeded } type StripePaymentIntent { stripeIntentId: String! clientSecret: String! stripeConnectAccountId: String! customerSessionClientSecret: String subtotal: SafeInt! tip: SafeInt! stripeProcessingFee: SafeInt! customerProcessingFee: SafeInt! applicationFee: SafeInt! amount: SafeInt! currency: Currency! paymentMethodType: StripePaymentMethodType walletType: StripeWalletType } type StripeSetupIntent { stripeIntentId: String! clientSecret: String! stripeConnectAccountId: String! customerSessionClientSecret: String! } type StripeIntent { stripeIntentId: String! stripeCustomerId: String! clientSecret: String @deprecated customerSessionClientSecret: String @deprecated type: StripeIntentType! customerId: SafeInt! invoiceId: SafeInt estimateId: SafeInt status: StripePaymentIntentStatus! errorType: String errorCode: String errorMessage: String createdAt: DateTime! updatedAt: DateTime! } """Company connected stripe information""" type Stripe { id: SafeInt! publishableKey: String stripeAccountId: String } enum Currency { USD CAD EUR GBP AUD CHF CZK DKK HKD HUF JPY NOK NZD PLN SEK SGD RUB UAH ZAR CNY INR SRD } """ Mirrors Stripe subscription statuses plus CANCEL_REQUESTED for scheduled cancellations """ enum SubscriptionStatus { CURRENT PAST_DUE UNPAID CANCELLED CANCEL_REQUESTED INCOMPLETE INCOMPLETE_EXPIRED TRIALING PAUSED } """Why a customer is canceling their subscription.""" enum CancellationReason { TOO_EXPENSIVE HARD_TO_USE MISSING_FEATURES TECHNICAL_ISSUES DIFFICULT_ONBOARDING OTHER } """What the customer valued most, even though they're leaving.""" enum SatisfactionHighlight { EASE_OF_USE CUSTOMER_SUPPORT GREAT_COMMUNITY VALUE_FOR_PRICE GREAT_ONBOARDING OTHER } """Last payment failure details (resolved on demand from Stripe)""" type PaymentError { """Stripe decline code (e.g., 'card_declined', 'insufficient_funds')""" code: String """Stripe decline reason code (e.g., 'generic_decline')""" declineCode: String """Human-readable error message""" message: String } """ The one subscription operation the company may perform. Mutations enforce it too. - CREATE: no live subscription; choose a plan with createPlanSubscription. - UPDATE: healthy subscription; plan and seat changes are allowed. - RESUME: payment is owed; pay it with resumeSubscription. Plan and seat changes are rejected until then. - NONE: a payment is in flight or the subscription is paused; neither create nor update applies. """ enum BillingAction { CREATE UPDATE RESUME NONE } """ What a failed invoice is paying for. - PLAN: the invoice carries a base-plan charge (alone or with add-ons). Short grace period. - ADD_ON: the invoice carries only add-on charges (seats and/or SMS metered usage); the plan itself is paid up. Longer grace period, and the customer never has to pay the plan again to recover. """ enum InvoiceFailureKind { PLAN ADD_ON } enum FailedInvoiceItemType { PLAN SEAT SMS } """One charge on the failed invoice""" type FailedInvoiceItem { """ Customer-facing label, e.g. '3 × Additional Employee Login (at $15.00 / month)' """ description: String! type: FailedInvoiceItemType! """ Net charge (cents) for this item: after discounts and prorations, before tax. Items need not add up to the invoice's amountDueCents, which includes tax. """ amountCents: SafeInt! } """One unpaid invoice of a past-due subscription""" type OutstandingInvoiceDetail { """Stripe invoice ID""" invoiceId: String! """ Amount (cents) Stripe will charge when the invoice is paid, tax included """ amountDueCents: SafeInt! """When the invoice was created""" createdAt: DateTime! """ Whether this invoice charges for the plan or only for add-ons (seats, SMS usage) """ failureKind: InvoiceFailureKind! """ What the invoice charges for, plan first, then seats, then SMS. Empty when the invoice's lines could not be read. """ items: [FailedInvoiceItem!]! } """ Everything a past-due subscription has to pay before it is active again: the invoice that failed and any that built up behind it while the subscription was locked. `resumeSubscription` settles them together. """ type OutstandingInvoice { """Stripe invoice ID of the oldest unpaid invoice""" invoiceId: String! """ Total amount (cents) of every unpaid invoice, tax included. Show this, not the plan price. """ amountDueCents: SafeInt! """Lowercase ISO currency code (e.g. 'usd')""" currency: String! """ Whether the plan or only add-ons (seats, SMS usage) is unpaid: PLAN when any unpaid invoice has a plan line. Use it to say what is owed and that a yearly plan does not need paying again. """ failureKind: InvoiceFailureKind! """ What the unpaid invoices charge for, oldest invoice first (plan, then seats, then SMS within each). Empty when no invoice's lines could be read; fall back to a generic message. Use `invoices` for a breakdown per invoice. """ failedItems: [FailedInvoiceItem!]! """ When the oldest unpaid invoice was created, i.e. when the payment first failed. Use this as the 'failed on' date: planPeriodEndsOn is wrong for an add-on failure on a yearly plan. """ failedAt: DateTime! """Each unpaid invoice with its own items, oldest first""" invoices: [OutstandingInvoiceDetail!]! """ Whether a cancellation is scheduled on the subscription. A past-due subscription has status PAST_DUE even when one is scheduled, so use `status === CANCEL_REQUESTED || outstandingInvoice.cancellationScheduled` to decide whether to promise a renewal. """ cancellationScheduled: Boolean! } type CompanySubscription { """Current plan SKU""" planPriceId: String! """Scheduled downgrade plan SKU""" pendingPriceId: String """Subscription status""" status: SubscriptionStatus! """Stripe subscription ID""" stripeSubscriptionId: String """Amount past due in cents""" pastDueAmountCents: SafeInt! """Grace period end date for failed payments""" gracePeriodEndsOn: DateTime """Current billing period end date""" planPeriodEndsOn: DateTime """Next charge date""" nextChargeOn: DateTime """Default payment method expiration month""" cardExpMonth: Int """Default payment method expiration year""" cardExpYear: Int """Plan details derived from planPriceId""" plan: Plan! """ Pending plan details derived from pendingPriceId (if scheduled downgrade exists) """ pendingPlan: Plan """Tier derived from planPriceId""" tier: Tier! """ Pending tier derived from pendingPriceId (if scheduled downgrade exists) """ pendingTier: Tier """Subscription created date""" createdAt: DateTime! """Subscription last updated date""" updatedAt: DateTime! """ Last payment error (resolved on demand from Stripe; null when status is CURRENT/TRIALING) """ lastPaymentError: PaymentError """ Which subscription operation the UI should offer. Derived from the stored status, so it needs no Stripe call. Route on this instead of status/accountStatus. """ billingAction: BillingAction! """ What to pay to recover a past-due subscription: every unpaid invoice (resolved on demand from Stripe). Null unless billingAction is RESUME, when nothing is owed, or when Stripe is unreachable. """ outstandingInvoice: OutstandingInvoice """ Amount (cents) of the next upcoming invoice from Stripe, reflecting any active discounts or prorations. Null for free tier, cancelled subscriptions, or when Stripe is unreachable. Prefer this over plan.price for billing UI. """ nextInvoiceAmountCents: SafeInt """ Start of the current monthly billing period (sourced from Stripe). Null until the company is on flexible billing. """ monthlyPeriodStartedAt: DateTime """ End of the current monthly billing period (sourced from Stripe). Null until the company is on flexible billing. """ monthlyPeriodEndsAt: DateTime """ Approximate usage and charges for the current billing period. Null if the period has not yet been populated. """ monthlyUsage: SubscriptionMonthlyUsage } """ Approximate usage and cost picture for the current monthly billing period. """ type SubscriptionMonthlyUsage { """ Number of outbound SMS messages sent this billing period (PENDING + POSTED only) """ smsMessagesSent: SafeInt! """ Number of SMS segments accumulated this billing period (PENDING + POSTED only) """ smsSegmentsSent: SafeInt! """ Approximate SMS charge for this period based on segments beyond the included allowance """ approximateSmsCost: Decimal! """Number of currently active user seats""" activeUserSeats: SafeInt! """ Additional seats beyond the tier's included allowance (max(activeUserSeats - tier.includedUserSeats, 0)) """ additionalUserSeats: SafeInt! """Approximate charge for additional user seats this billing cycle""" approximateUserSeatCost: Decimal! } type TempFile { key: String! contentType: String! uploadUrl: String! downloadUrl: String expires: SafeInt! } input TimeEntryReportOrderBy { userId: SortOrder categoryId: SortOrder eventId: SortOrder customerId: SortOrder propertyId: SortOrder _count: TimeEntryReportCountOrderBy _sum: TimeEntryReportSumOrderBy _avg: TimeEntryReportAvgOrderBy _min: TimeEntryReportMinOrderBy _max: TimeEntryReportMaxOrderBy count: TimeEntryReportCountOrderBy @deprecated(reason: "Use _count") sum: TimeEntryReportSumOrderBy @deprecated(reason: "Use _sum") avg: TimeEntryReportAvgOrderBy @deprecated(reason: "Use _avg") min: TimeEntryReportMinOrderBy @deprecated(reason: "Use _min") max: TimeEntryReportMaxOrderBy @deprecated(reason: "Use _max") } input TimeEntryReportCountOrderBy { id: SortOrder } input TimeEntryReportSumOrderBy { totalSeconds: SortOrder paymentAmount: SortOrder bonusPaymentAmount: SortOrder } input TimeEntryReportAvgOrderBy { totalSeconds: SortOrder hourlyRate: SortOrder paymentAmount: SortOrder bonusPaymentAmount: SortOrder } input TimeEntryReportMinOrderBy { totalSeconds: SortOrder hourlyRate: SortOrder paymentAmount: SortOrder startedAt: SortOrder endedAt: SortOrder } input TimeEntryReportMaxOrderBy { totalSeconds: SortOrder hourlyRate: SortOrder paymentAmount: SortOrder startedAt: SortOrder endedAt: SortOrder } type TimeEntryReportCountAggregate { id: SafeInt! } type TimeEntryReportSumAggregate { totalSeconds: SafeInt paymentAmount: Decimal bonusPaymentAmount: Decimal } type TimeEntryReportAvgAggregate { totalSeconds: Float hourlyRate: Decimal paymentAmount: Decimal bonusPaymentAmount: Decimal } type TimeEntryReportMinAggregate { totalSeconds: SafeInt hourlyRate: Decimal paymentAmount: Decimal startedAt: LocalDateTime endedAt: LocalDateTime } type TimeEntryReportMaxAggregate { totalSeconds: SafeInt hourlyRate: Decimal paymentAmount: Decimal startedAt: LocalDateTime endedAt: LocalDateTime } enum UserStatus { BLOCKED ACTIVE SUSPENDED ADDON_ACTIVE } enum UserRole { OWNER ADMIN EMPLOYEE } input UserRoleFilter { equals: UserRole not: UserRole in: [UserRole!] notIn: [UserRole!] } input UserFilter { id: IntFilter email: StringFilter firstName: StringFilter lastName: StringFilter status: UserStatus isDeleted: Boolean lastLogin: DateTimeFilter role: UserRoleFilter events: EventFilter excludeFromCapacity: Boolean """ Searches across email, contactEmail, phone, cell, firstName, lastName (case-insensitive contains), and id (exact match) """ search: String AND: [UserFilter!] OR: [UserFilter!] NOT: UserFilter } input UserSort { id: SortOrder email: SortOrder firstName: SortOrder lastName: SortOrder role: SortOrder status: SortOrder isDeleted: SortOrder lastLogin: SortOrder } """ Shared User type used by both Admin and Company APIs. Company extends this with estimateHistory, events, crews, permissions. """ type User { id: SafeInt! email: String! firstName: String! lastName: String! role: UserRole! status: UserStatus! address: Address! phone: String! fax: String! cell: String! contactEmail: String! notes: String title: String! picture: File! lastLogin: DateTime! votes: SafeInt! isDeleted: Boolean! license: String tags: String! partner: SafeInt! latitude: String @deprecated(reason: "Use lastKnownLocation instead") longitude: String @deprecated(reason: "Use lastKnownLocation instead") """ The user's last known GPS location (e.g. from the mobile app). Not based on their home/mailing address. Null if never reported. """ lastKnownLocation: Location """ The employee's on-file address resolved to map coordinates. Suitable as a route start/end point. Null if address is blank or geocoding fails. Use this — not lastKnownLocation — for route endpoint selection. lastKnownLocation is a GPS ping, not where the employee starts the day. """ addressLocation: Location locationStoredAt: DateTime locationLastFetchAttemptedAt: DateTime inactiveDate: DateTime dashboardLimit: SafeInt! """ Per-user day-view color map (JSON array). Controls event colors specifically in the day view of the scheduler. """ dayColor: String! """ Default fallback color for the scheduler when no per-category color is set (JSON array). """ scheduleDefaultColor: String! """ User's display color (hex string). Used for the user chip/avatar in the scheduler. """ color: String allowUpsells: Boolean! company: Company! estimateHistory(where: EstimateHistoryFilter, orderBy: [EstimateHistorySort!], take: SafeInt = 1000, skip: SafeInt): [EstimateHistory] permissions: Permissions! """Per-date available hours overrides for this user.""" dayOverrides(where: UserDayOverrideFilter, orderBy: [UserDayOverrideSort!], take: SafeInt = 1000, skip: SafeInt): [UserDayOverride!]! hireDate: Date! birthDate: Date! rate: Decimal! """ Pay rate period/unit. TODO: convert to enum UserRateType (Hour | Day | Week | Month | Year). Check P4P and Field Force for breaking GraphQL query changes before migrating. """ rateType: String! """ Pay performance type. TODO: convert to enum UserPayPerfType. Values: 1=Rate, 2=Rate+flat per job, 3=Rate+percent per job, 4=Flat rate, 5=Flat+percent per job, 6=Percent per job. Check P4P and Field Force for breaking GraphQL query changes before migrating. """ payPerfType: SafeInt! """ Pay performance amount — numeric value that works with payPerfType (e.g. the percentage or flat amount). TODO: rename once payPerfType is an enum. """ payPerf: String! """ Default daily available hours used for capacity planning and scheduling (e.g. 8.0 = a standard full day). """ dailyCapacityHours: Decimal! """ Maximum billable hours per week for this user. Used as a cap when rolling up company-wide capacity. """ weeklyCapacityHours: SafeInt! """ When true, this user is excluded from company-wide capacity calculations (e.g. office staff, non-field employees). """ excludeFromCapacity: Boolean! clockInRequired: Boolean! """ Per-event-type color and contrast settings for the schedule view, including the high-contrast text boolean. Unset keys fall back to the default (#3a87ab, contrast: false). """ scheduleColorSettings: ScheduleColorSettings """ Number of non-deleted events assigned to this user. Pass where to narrow the count. """ eventCount(where: EventFilter): SafeInt! """ Events assigned to this user. Defaults to non-deleted events. Pass where: { isDeleted: true } to include deleted events. """ events(where: EventFilter = {isDeleted: false}, orderBy: [EventSort!], take: SafeInt = 1000, skip: SafeInt): [Event] """Recurring event series this user is assigned to.""" recurringEvents(where: RecurringEventFilter, orderBy: [RecurringEventSort!], take: SafeInt = 1000, skip: SafeInt): [RecurringEvent] """Crews this user belongs to. Defaults to non-deleted crews.""" crews(where: CrewFilter = {isDeleted: false}, orderBy: [CrewSort!], take: SafeInt = 1000, skip: SafeInt): [Crew] } type Permissions { calendar: [PermissionCalendar!]! customers: [Permission!]! estimates: [Permission!]! expenses: [Permission!]! invoices: [Permission!]! payments: [Permission!]! settings: [Permission!]! timeTracking: [PermissionTimeTracking!]! automations: [Permission!]! backup: [Permission!]! billingSettings: [Permission!]! calculator: [Permission!]! chemicalApplications: [Permission!]! chemicalTracking: [Permission!]! cloudStorage: [Permission!]! contracts: [Permission!]! crews: [Permission!]! customFields: [Permission!]! dispatchBoard: [Permission!]! documents: [Permission!]! emails: [Permission!]! users: [Permission!]! equipment: [Permission!]! items: [Permission!]! kpiCockpit: [Permission!]! marketplace: [Permission!]! notes: [Permission!]! properties: [Permission!]! reports: [Permission!]! texts: [Permission!]! visitForms: [Permission!]! vendors: [Permission!]! franchises: [Permission!]! @deprecated(reason: "Not referenced in copilot-api or copilot PHP; not in pages table seed — likely legacy") canSkipEvents: Boolean! @deprecated(reason: "Use Permissions.calendar.canSkipEvents") } """ Calendar-specific permissions for the CALENDAR page. All employees can view and work on events assigned to them. ACCESS_ALL = can see all events (not just own) COMPLETE_AND_INVOICE = can complete and invoice events (accessLevel >= 2) CREATE/UPDATE = can see prices when completing/invoicing (accessLevel >= 3, implies COMPLETE_AND_INVOICE) SKIP = can mark events as skipped DELETE = can delete events """ enum PermissionCalendar { ACCESS_ALL CREATE UPDATE COMPLETE_AND_INVOICE SKIP DELETE } enum Permission { READ UPDATE CREATE DELETE } """ Time-tracking-specific permissions. Always includes READ_OWN. READ_OWN = can view own time entries (default for all) READ_ALL = can view all users' time entries (implies READ_OWN) EDIT_OWN = can edit own time entries (implies READ_OWN) EDIT_ALL = can view and edit all users' time entries (implies all others) """ enum PermissionTimeTracking { READ_OWN READ_ALL EDIT_OWN EDIT_ALL } """ Color and contrast-text setting for a single event type in the schedule view. """ type ScheduleColorSetting { color: String! """ Whether event text should render in high-contrast (dark) color against this background. Defaults to false. """ contrast: Boolean! } """ Per-event-type color and contrast settings for the schedule view. Defaults are used for any key not explicitly set by the user. """ type ScheduleColorSettings { visit: ScheduleColorSetting! todo: ScheduleColorSetting! meeting: ScheduleColorSetting! maintenance: ScheduleColorSetting! } enum VendorStatus { ACTIVE INACTIVE DELETED } type Vendor { id: SafeInt! companyId: SafeInt! firstName: String! lastName: String! companyName: String email: String! phone: String cell: String address: Address description: String isDeleted: Boolean! status: VendorStatus """ Type references the customer_types table (dynamic, company-specific). Use the raw integer ID to filter. """ type: SafeInt customerType: CustomerType vendorNumber: String registeredAt: DateTime updatedAt: DateTime } input VendorFilter { id: IntFilter isDeleted: Boolean status: VendorStatus firstName: StringFilter lastName: StringFilter companyName: StringFilter email: StringFilter city: StringFilter state: StringFilter AND: [VendorFilter!] OR: [VendorFilter!] NOT: VendorFilter } input VendorSort { id: SortOrder firstName: SortOrder lastName: SortOrder companyName: SortOrder } type AppEvent { id: SafeInt! companyId: SafeInt customerId: SafeInt type: AppEventType! objectId: SafeInt objectType: ObjectType! actorType: AppEventActorType! actorId: SafeInt data: JSON initiatedAt: DateTime! createdAt: DateTime } input AppEventSort { initiatedAt: SortOrder createdAt: SortOrder } type Area { id: SafeInt! name: String! color: String! """ Polygon boundary as an ordered list of coordinates. The address field on each Coordinate is always null for area points. """ points: [Coordinate!]! """ Company-wide list/draw position, 1-based. New areas are appended (max + 1, so a company's first area is 1); use reorderAreas to change the order of the whole set. """ sortOrder: Int! createdAt: DateTime! updatedAt: DateTime! deletedAt: DateTime } input AreaFilter { id: IntFilter name: StringFilter """ Soft-deleted areas are always excluded unless this is true, which returns only soft-deleted areas. """ isDeleted: Boolean AND: [AreaFilter!] OR: [AreaFilter!] NOT: AreaFilter } input AreaSort { id: SortOrder name: SortOrder sortOrder: SortOrder createdAt: SortOrder } input CreateAreaInput { name: String! color: String! """ Polygon boundary points. The label and address fields on CoordinateInput are ignored for area points. A closing point that repeats the first point is dropped. """ points: [CoordinateInput!]! """ 1-based list position; values below 1 are rejected (400). Defaults to the end of the list (current max + 1, or 1 for the first area). """ sortOrder: Int } input UpdateAreaInput { name: String color: String """ Polygon boundary points. The label and address fields on CoordinateInput are ignored for area points. A closing point that repeats the first point is dropped. """ points: [CoordinateInput!] """1-based list position; values below 1 are rejected (400).""" sortOrder: Int } input CreateCrewInput { name: String! notes: String color: String tags: [String!] """Start location. Omit to leave unset. Coordinates come from the client.""" routeStartLocation: LocationInput """End location. Omit to leave unset. Independent of the start.""" routeEndLocation: LocationInput } input UpdateCrewInput { name: String notes: String color: String tags: [String!] """Start location. Omit to keep the current value; null clears it.""" routeStartLocation: LocationInput """ End location. Omit to keep the current value; null clears it. Independent of the start. """ routeEndLocation: LocationInput } input DynamicCustomFieldFilter { objectType: CustomFieldObjectType } type CustomFieldTotal { fieldId: SafeInt! total: Decimal! } """ A single row in a customer report. Each row represents one group determined by the selected dimensions. If none are selected the query returns a single aggregate row covering all customers. """ type CustomerReportRow { customerTypeId: SafeInt sourceId: SafeInt status: CustomerStatus isDeleted: Boolean country: String state: String city: String zip: String """Resolved customer type for this group.""" customerType: CustomerType """Resolved lead source for this group.""" source: CustomerSource _count: CustomerReportCountAggregate _sum: CustomerReportSumAggregate _avg: CustomerReportAvgAggregate _min: CustomerReportMinAggregate _max: CustomerReportMaxAggregate count: CustomerReportCountAggregate @deprecated(reason: "Use _count") sum: CustomerReportSumAggregate @deprecated(reason: "Use _sum") avg: CustomerReportAvgAggregate @deprecated(reason: "Use _avg") min: CustomerReportMinAggregate @deprecated(reason: "Use _min") max: CustomerReportMaxAggregate @deprecated(reason: "Use _max") } type CustomerReportCountAggregate { id: SafeInt } type CustomerReportSumAggregate { credit: Decimal } type CustomerReportAvgAggregate { credit: Decimal discountPercent: Decimal } type CustomerReportMinAggregate { credit: Decimal } type CustomerReportMaxAggregate { credit: Decimal } input CustomerReportOrderBy { customerTypeId: SortOrder sourceId: SortOrder status: SortOrder isDeleted: SortOrder country: SortOrder state: SortOrder city: SortOrder zip: SortOrder _count: CustomerReportCountOrderBy _sum: CustomerReportSumOrderBy _avg: CustomerReportAvgOrderBy _min: CustomerReportMinOrderBy _max: CustomerReportMaxOrderBy } input CustomerReportCountOrderBy { id: SortOrder } input CustomerReportSumOrderBy { credit: SortOrder } input CustomerReportAvgOrderBy { credit: SortOrder discountPercent: SortOrder } input CustomerReportMinOrderBy { credit: SortOrder } input CustomerReportMaxOrderBy { credit: SortOrder } type CustomerSource { id: SafeInt! name: String! isVisible: Boolean } """ Input for creating or updating a customer. All fields are optional for updates; provide at minimum fullName or firstName+lastName when creating. """ input CustomerInput { """Display name for the customer (e.g. company name or full name)""" fullName: String firstName: String lastName: String """Primary contact email""" email: String """Business or landline phone number.""" phone: String """ Mobile/cell phone number. Set this field to enable SMS for the customer. For US customers, this value is used directly for SMS. For international customers, ensure country is also set — cellFull (used for international SMS) is auto-computed from cell + country. Never set cellFull directly. """ cell: String fax: String address: AddressInput """Customer type ID (references customer_types table)""" customerTypeId: SafeInt """Customer source ID (references customer_sources table)""" sourceId: SafeInt description: String discountPercent: Decimal isReceivePhone: Boolean isReceiveEmail: Boolean isReceiveText: Boolean """CC email address 1""" ccEmail: String """CC email address 2""" ccEmail1: String """CC email address 3""" ccEmail2: String """CC email address 4""" ccEmail3: String """0 = taxable, 1 = exempt (see company settings)""" isTaxExempt: SafeInt """Email template ID to use for invoice delivery to this customer""" invoiceEmailTemplateId: SafeInt """SMS template ID to use for invoice delivery to this customer""" invoiceSmsTemplateId: SafeInt """SMS template ID to use for estimate delivery to this customer""" estimateSmsTemplateId: SafeInt """ Optional custom field values to set on create or update. Each fieldId must be a DynamicCustomField that belongs to this company and applies to customers. Unrecognised fieldIds throw BadRequest. On create, fields omitted here are seeded with empty/zero defaults. On update, supplying this field upserts by fieldId; omitting leaves values untouched. """ customFields: [CustomFieldInput!] """ Replace-set of tag names. Supplying this field replaces the current tag set; omitting it leaves tags untouched. """ tags: [String!] """ Same AutoChargePolicy enum as Customer.autoChargePolicy (INHERIT / DISABLED / USE_COMPANY). """ autoChargePolicy: AutoChargePolicy } enum CustomerEmailSendability { """Customer has an email and has not unsubscribed""" SENDABLE """ Customer has an unsubscribe tag (Unsubscribe or the legacy Unsubscribed) """ UNSUBSCRIBED """Customer has no email on file""" NO_EMAIL } enum CustomerSmsSendability { SENDABLE NO_PHONE INVALID_PHONE OPTED_OUT LANDLINE } type EmailTemplate { id: SafeInt! name: String! subject: String! """Beefree design JSON (legacy rows may be double JSON-encoded),""" content: String! """HTML body, when the template has one""" html: String } type EstimateReportRow { status: EstimateStatus customerId: SafeInt date: Date isSent: Boolean customer: Customer _count: EstimateReportCountAggregate _sum: EstimateReportSumAggregate _avg: EstimateReportAvgAggregate _min: EstimateReportMinAggregate _max: EstimateReportMaxAggregate count: EstimateReportCountAggregate @deprecated(reason: "Use _count") sum: EstimateReportSumAggregate @deprecated(reason: "Use _sum") avg: EstimateReportAvgAggregate @deprecated(reason: "Use _avg") min: EstimateReportMinAggregate @deprecated(reason: "Use _min") max: EstimateReportMaxAggregate @deprecated(reason: "Use _max") } enum EstimateSentByType { EMAIL MAIL DROPPED_OFF NOT_SENT } enum EstimateLineItemEventStatus { """Not scheduled""" NO_EVENT """Scheduled and linked to event""" EVENT_CREATED """Manually marked as scheduled with no event""" EVENT_OVERRIDE """ Only currently used for line items that were created before October 1st """ UNKNOWN } input EstimateLineItemEventStatusFilter { equals: EstimateLineItemEventStatus not: EstimateLineItemEventStatus in: [EstimateLineItemEventStatus!] notIn: [EstimateLineItemEventStatus!] } input CreateEstimateInput { number: SafeInt purchaseOrder: String customerId: SafeInt! date: DateTime internalNotes: String title: String notes: String terms: String discountPercent: Decimal requestId: SafeInt tags: String """If null, this will inherit from the Company setting.""" payInFullEnabled: Boolean """ Optional custom field values to set at creation time. Each fieldId must be a DynamicCustomField that belongs to this company and applies to estimates. Unrecognised fieldIds throw BadRequest. Fields omitted here are seeded with empty/zero defaults. """ customFields: [CustomFieldInput!] } input UpdateEstimateInput { number: SafeInt purchaseOrder: String customerId: SafeInt date: DateTime internalNotes: String title: String notes: String terms: String discountPercent: Decimal requestId: SafeInt tags: String """If null, this will inherit from the Company setting.""" payInFullEnabled: Boolean """ Custom field values to set. Upserts by fieldId; omitting leaves custom fields untouched. Each fieldId must be a DynamicCustomField that belongs to this company and applies to estimates. """ customFields: [CustomFieldInput!] } input EstimateLineItemInput { name: String description: String date: DateTime price: Decimal quantity: Decimal taxPercent1: Decimal taxPercent2: Decimal itemId: SafeInt sortOrder: SafeInt propertyId: SafeInt budgetedHours: Decimal } enum EstimateSendMethod { EMAIL MAIL SMS } enum EstimateSendStatus { QUEUED SENT ERROR } type EstimateSendResult { estimateId: SafeInt! status: EstimateSendStatus! errorMessage: String } type EstimateSendResults { results: [EstimateSendResult!]! } """ A category that can be assigned to an event (e.g. a TODO or Meeting category). """ type EventCategory { id: SafeInt! name: String! """The event type this category applies to (e.g. TODO, MEETING).""" type: EventType! companyId: SafeInt! } input EventCategoryFilter { id: IntFilter name: StringFilter type: EventTypeFilter isDeleted: Boolean } input EventCategorySort { id: SortOrder name: SortOrder type: SortOrder } type EventForm { id: SafeInt! eventId: SafeInt! formId: SafeInt formName: String submittedContent: String submittedSignature: String submittedByUserId: SafeInt createdAt: DateTime updatedAt: DateTime content: String! form: Form event: Event submittedByUser: User } enum FormEditorType { COMPANY_OWNER EMPLOYEES PROPERTY_OWNER ALL } type Form { id: SafeInt! name: String! content: String! editors: [FormEditorType!]! } """Deprecated. Use Event.users instead.""" type EventEmployee { id: SafeInt! eventId: SafeInt! employeeUserId: SafeInt! """ The user assigned to this event. May be null if employeeUserId is 0 in the database. """ user: User createdAt: DateTime! } input EventEmployeeFilter { employeeUserId: IntFilter } type TimeEntryTotals { pastTimeEntries: SafeInt! pastTimeEntrySeconds: SafeInt! runningTimers: SafeInt! runningTimerSeconds: SafeInt! totalSeconds: SafeInt! at: DateTime! } type DispatchNote { message: String! author: String! date: DateTime! } input DispatchNoteInput { message: String! date: DateTime } type EventReportRow { status: EventStatus type: EventType customerId: SafeInt crewId: SafeInt propertyId: SafeInt recurringEventId: SafeInt completedByUserId: SafeInt categoryId: SafeInt budgetedHours: Decimal isInvoiceable: Boolean """ The calendar year of the event's start date, in the company's timezone. Note: events created via automated/cron paths (e.g. QuickBooks sync, auto-bill) may reflect UTC instead. """ year: Int """ The calendar month (1–12) of the event's start date, in the company's timezone. Note: events created via automated/cron paths (e.g. QuickBooks sync, auto-bill) may reflect UTC instead. """ month: Int startDate: Date customer: Customer crew: Crew property: Property completedByUser: User _count: EventReportCountAggregate _sum: EventReportSumAggregate _avg: EventReportAvgAggregate _min: EventReportMinAggregate _max: EventReportMaxAggregate """@deprecated Use _count""" count: EventReportCountAggregate @deprecated(reason: "Use _count") """@deprecated Use _sum""" sum: EventReportSumAggregate @deprecated(reason: "Use _sum") """@deprecated Use _avg""" avg: EventReportAvgAggregate @deprecated(reason: "Use _avg") """@deprecated Use _min""" min: EventReportMinAggregate @deprecated(reason: "Use _min") """@deprecated Use _max""" max: EventReportMaxAggregate @deprecated(reason: "Use _max") } """ Subset of EventStatus valid on create. CLOSED, SKIPPED, and CANCELLED are not valid initial states. """ enum CreateEventStatus { OPEN WAITLISTED } input CreateEventLineItemInput { itemId: SafeInt! price: Decimal! quantity: Decimal! """ Budgeted hours for this service. Contributes to the event-level budgetedHours, which is computed as the sum of all line item budgetedHours for VISIT events. """ budgetedHours: Decimal """ Tax rate as a percentage (e.g. 8.5 for 8.5%). Optional — omit to create the line item without tax. If tax applies, use the lookupTaxRate query to resolve the correct rate for the customer / property / item combination. total is computed server-side as price x quantity x (1 + tax / 100). """ tax: Decimal description: String """Links this line item back to the source estimate line.""" estimateLineId: SafeInt } input CreateEventInput { """ The type of event. Determines which fields are required. - VISIT requires customerId, lineItems, and either propertyId or location (not both). - TODO and MEETING require only title and startDate; all other fields are optional. - MAINTENANCE requires equipmentId (title is auto-generated from equipment name + model). - WORK_REQUEST requires customerId and workRequestId. Line items may only be provided for VISIT. """ type: EventType! """ Required for all types except MAINTENANCE (auto-generated from equipment name + model). """ title: String """ Custom location for this event, written directly with no geocoding. Mutually exclusive with propertyId — pass one or the other, not both. When provided for a VISIT, propertyId is not required. """ location: LocationInput """ Initial status. Only OPEN and WAITLISTED are valid on create. Defaults to OPEN. """ status: CreateEventStatus """ISO 8601 date (YYYY-MM-DD).""" startDate: LocalDate! """ If provided, the event has a specific time of day (hasTime = true). ISO 8601 time (HH:MM:SS). """ startTime: LocalTime """ Inclusive last day for multi-day events, or waitlist scheduling window end. Only valid when startTime is not set. Exact last active date for a multi-day event, inclusive for every event status. Null for a single-day event. ISO 8601 date (YYYY-MM-DD). """ endDate: LocalDate """ Scheduled duration in hours. Determines endTime when startTime is provided. Must not be set for VISIT events — budgetedHours on VISIT events is derived automatically as the sum of the line items' budgetedHours. Can be updated/prorated later based on time tracked. Defaults to 0 for non-VISIT types. """ budgetedHours: Decimal """Required for VISIT and WORK_REQUEST. Optional for TODO and MEETING.""" customerId: SafeInt """ Optional for VISIT (provide propertyId or location, not both). Optional for other types. """ propertyId: SafeInt """Required for MAINTENANCE only.""" equipmentId: SafeInt """ Required for WORK_REQUEST type. Optional for VISIT (links the visit to a work request without requiring the WORK_REQUEST type). """ workRequestId: SafeInt """ If provided, the linked estimate is automatically marked as ACCEPTED and estimate line items are linked to the created event line items. """ estimateId: SafeInt contractId: SafeInt crewId: SafeInt """User IDs to assign to this event.""" userIds: [SafeInt!] description: String categoryId: SafeInt """ Freeform location label override (not a property lookup). @deprecated Use location.name instead. """ locationName: String @deprecated(reason: "Use location.name instead") """Hex color string (e.g. '#FF0000'). Defaults to empty string.""" color: String isInvoiceable: Boolean tags: [String!] """ S3 temp file keys to attach as media. Obtain keys via the createUpload mutation: call createUpload, PUT the file to the returned uploadUrl, then pass the key here. """ mediaFileKeys: [String!] """MAINTENANCE type only. Defaults to 0.""" maintenanceCost: Decimal """ Form IDs to attach on creation. Forms can also be managed after creation via addEventForm / removeEventForm mutations. """ formIds: [SafeInt!] """ Required for VISIT. At least one item must be provided. Only valid for VISIT — other types must omit lineItems. total is computed server-side — do not send it. """ lineItems: [CreateEventLineItemInput!] """ Optional custom field values to set at creation time. Each fieldId must be a DynamicCustomField that belongs to this company and applies to events. Unrecognised fieldIds throw BadRequest. Fields omitted here are seeded with empty/zero defaults. """ customFields: [CustomFieldInput!] } enum EventEmailSendStatus { """Queued for delivery by the existing PHP email worker.""" QUEUED ERROR } type EventEmailSendResult { eventId: SafeInt! status: EventEmailSendStatus! errorMessage: String } type EventEmailSendResults { results: [EventEmailSendResult!]! } enum EventSmsSendStatus { """Queued for sending; delivery shows on the SMS thread once sent.""" QUEUED ERROR } type EventSmsSendResult { eventId: SafeInt! status: EventSmsSendStatus! errorMessage: String } type EventSmsSendResults { results: [EventSmsSendResult!]! } """The series-level status of a recurring event, derived at query time.""" enum RecurringSeriesStatus { """Series is active and generating visits on schedule.""" ACTIVE """ Series is paused because today falls within its assigned off-season window. """ PAUSED """Series has been permanently cancelled.""" CANCELLED } """ A named, annually-recurring date range used to pause recurring event series. Off-seasons are company-wide templates (e.g. "Winter Break: Nov 1 – Feb 28"). No year is stored — the range repeats every year. """ type OffSeason { id: SafeInt! name: String! """Month the off-season starts (1–12).""" startMonth: Int! """Day of the month the off-season starts.""" startDay: Int! """Month the off-season ends (1–12).""" endMonth: Int! """Day of the month the off-season ends.""" endDay: Int! } input OffSeasonFilter { id: IntFilter name: StringFilter } input OffSeasonSort { id: SortOrder name: SortOrder startMonth: SortOrder startDay: SortOrder endMonth: SortOrder endDay: SortOrder } type RecurringEventUser { id: SafeInt! companyId: SafeInt! recurringEventId: SafeInt! userId: SafeInt! user: User! } type InvoiceReportRow { status: InvoiceStatus customerId: SafeInt propertyId: SafeInt date: Date isSent: Boolean source: String customer: Customer property: Property _count: InvoiceReportCountAggregate _sum: InvoiceReportSumAggregate _avg: InvoiceReportAvgAggregate _min: InvoiceReportMinAggregate _max: InvoiceReportMaxAggregate count: InvoiceReportCountAggregate @deprecated(reason: "Use _count") sum: InvoiceReportSumAggregate @deprecated(reason: "Use _sum") avg: InvoiceReportAvgAggregate @deprecated(reason: "Use _avg") min: InvoiceReportMinAggregate @deprecated(reason: "Use _min") max: InvoiceReportMaxAggregate @deprecated(reason: "Use _max") } """ QuickBooks item mapping sync state for an invoice line item or customer module onboarding. """ enum QuickbooksSyncStatus { """Item has been matched in QuickBooks but not yet confirmed""" MAPPING_PENDING """Item is fully mapped and confirmed in QuickBooks""" MAPPING_COMPLETE """Customer QB module onboarding has been initiated but not yet completed""" MODULE_MAPPING_PENDING """Customer QB module onboarding is complete""" MODULE_MAPPING_COMPLETE } enum InvoiceSendMethod { EMAIL SMS SNAIL_MAIL } input UpdateInvoiceLineItemInput { """ID of the service item from the catalog. Optional.""" itemId: SafeInt """Display name for this line item.""" name: String """Description for this line item.""" description: String """Unit price.""" price: Decimal """Quantity.""" quantity: Decimal """Primary tax percent (e.g. 8.5 for 8.5%).""" taxPercent1: Decimal """Secondary tax percent.""" taxPercent2: Decimal """Sort order for display.""" sortOrder: SafeInt """Link this line item to a specific property/asset.""" propertyId: SafeInt } input CreateInvoiceLineItemInput { """ID of the service item from the catalog. Optional.""" itemId: SafeInt """Display name for this line item.""" name: String! """Description for this line item. Defaults to empty string.""" description: String """Unit price.""" price: Decimal! """Quantity. Defaults to 1.""" quantity: Decimal """ Primary tax percent (e.g. 8.5 for 8.5%). Use the lookupTaxRate query to resolve the correct rate for the customer/item/property combination before submitting. Defaults to 0. """ taxPercent1: Decimal """Secondary tax percent. Defaults to 0.""" taxPercent2: Decimal """Sort order for display. Defaults to 0.""" sortOrder: SafeInt """Link this line item to a specific property/asset.""" propertyId: SafeInt } input CreateInvoiceInput { """ID of the customer this invoice is for.""" customerId: SafeInt! """Invoice date. ISO 8601 date (YYYY-MM-DD).""" date: LocalDate! """ Invoice-level discount percentage (e.g. 10 for 10%). Applied uniformly to all line items. Defaults to 0. """ discountPercent: Decimal """Optional title for the invoice.""" title: String """ Public notes shown on the invoice. If omitted, resolved from the customer's default notes, falling back to company settings. """ notes: String """ Payment terms shown on the invoice. If omitted, resolved from the customer's default terms, falling back to company settings. """ terms: String """Internal notes — not visible to the customer.""" internalNotes: String """Purchase order number.""" purchaseOrder: String """Property/asset to associate with this invoice.""" propertyId: SafeInt """ Line items for the invoice. At least one is required. Subtotals, taxes, and totals are computed server-side — do not send them. Company-level global items are automatically appended server-side. """ lineItems: [CreateInvoiceLineItemInput!]! """ Optional S3 temp keys (include temp/ prefix). Attached after create; failures are logged and do not fail the mutation. """ mediaFileKeys: [String!] """ Optional custom field values to set at creation time. Each fieldId must be a DynamicCustomField that belongs to this company and applies to invoices. Unrecognised fieldIds throw BadRequest. Fields omitted here are seeded with empty/zero defaults. """ customFields: [CustomFieldInput!] } input ItemInput { categoryId: SafeInt! type: ItemType! = SERVICE name: String! description: String! = "" price: Decimal! = "0" quantity: Decimal! = "1" budgetedHours: Decimal = "1" tax1: Decimal! = "0" tax2: Decimal! = "0" unit: String! = "" supplierId: SafeInt materialCost: Decimal! = "0" equipmentCost: Decimal! = "0" subcontractCost: Decimal! = "0" hasInventory: Boolean! = false wholesaleCost: Decimal! = "0" inventoryOnHand: SafeInt! = 0 markup: Decimal! = "0" markupType: ItemMarkupType! = NONE isGlobal: Boolean! = false isTaxable: Boolean! = true color: String rateChargedP4p: Decimal! = "0" } """Nearly the same as ItemInput except every field is optional.""" input ItemUpdateInput { categoryId: SafeInt type: ItemType name: String description: String price: Decimal quantity: Decimal budgetedHours: Decimal tax1: Decimal tax2: Decimal unit: String supplierId: SafeInt materialCost: Decimal equipmentCost: Decimal subcontractCost: Decimal hasInventory: Boolean wholesaleCost: Decimal inventoryOnHand: SafeInt markup: Decimal markupType: ItemMarkupType isGlobal: Boolean isTaxable: Boolean color: String rateChargedP4p: Decimal } type PaymentReportRow { date: Date method: PaymentMethod customerId: SafeInt customer: Customer isRefund: Boolean _count: PaymentReportCountAggregate _sum: PaymentReportSumAggregate _avg: PaymentReportAvgAggregate _min: PaymentReportMinAggregate _max: PaymentReportMaxAggregate count: PaymentReportCountAggregate @deprecated(reason: "Use _count") sum: PaymentReportSumAggregate @deprecated(reason: "Use _sum") avg: PaymentReportAvgAggregate @deprecated(reason: "Use _avg") min: PaymentReportMinAggregate @deprecated(reason: "Use _min") max: PaymentReportMaxAggregate @deprecated(reason: "Use _max") } enum PaymentPayeeType { CUSTOMER VENDOR EQUIPMENT EMPLOYEE } type PaymentPayee { type: PaymentPayeeType! id: SafeInt! name: String! } type QuickDispatchTemplate { id: SafeInt! name: String! properties: [QuickDispatchTemplateProperty!]! """Total number of properties in this template.""" propertyCount: Int! } type QuickDispatchTemplateProperty { id: SafeInt! templateId: SafeInt! propertyId: SafeInt! """The property this entry targets.""" property: Property crewId: SafeInt """The crew assigned to this property, if any.""" crew: Crew """Whether visits created from this property should be invoiced.""" isInvoiceable: Boolean! """Optional visit note.""" note: String! """ Ordering within the template (0-based); also used as route order when dispatching. """ sortOrder: Int! """Sum of budgetedHours across all line items for this property.""" budgetedHours: Decimal! lineItems: [QuickDispatchTemplateLineItem!]! """Users assigned to this property's visits.""" users: [User!]! } type QuickDispatchTemplateLineItem { id: SafeInt! templatePropertyId: SafeInt! itemId: SafeInt! """The service/item being added.""" item: Item quantity: Int! price: Decimal! tax: Decimal! budgetedHours: Decimal! """ Stored line total: price × quantity × (1 + tax / 100), rounded to cents. """ total: Decimal! } input CreateQuickDispatchTemplateInput { name: String! """ Properties to add, each with at least one line item. A property without a sortOrder takes its position in this list (0-based). Omit to create an empty template and add properties later. """ properties: [AddQuickDispatchTemplatePropertyInput!] } input AddQuickDispatchTemplatePropertyInput { propertyId: SafeInt! crewId: SafeInt isInvoiceable: Boolean! note: String sortOrder: Int lineItems: [QuickDispatchTemplateLineItemInput!]! userIds: [SafeInt!] } input UpdateQuickDispatchTemplatePropertyInput { """Pass null to clear the assigned crew.""" crewId: SafeInt isInvoiceable: Boolean note: String sortOrder: Int } input QuickDispatchTemplateLineItemInput { itemId: SafeInt! quantity: Int! price: Decimal! tax: Decimal! budgetedHours: Decimal! } input UpdateQuickDispatchTemplateLineItemInput { quantity: Int price: Decimal tax: Decimal budgetedHours: Decimal } input PropertyTimeOverrideInput { """The QuickDispatchTemplateProperty ID this override applies to.""" templatePropertyId: SafeInt! startTime: LocalTime! } input DispatchQuickDispatchTemplateInput { templateId: SafeInt! date: LocalDate! """ Optional per-property start time overrides. Properties without an entry get no scheduled time. """ propertyTimeOverrides: [PropertyTimeOverrideInput!] } type DispatchedVisit { eventId: SafeInt! title: String! date: LocalDate! propertyId: SafeInt } input QuickDispatchTemplateFilter { name: StringFilter } input QuickDispatchTemplateSort { id: SortOrder name: SortOrder } input RouteReportOrderBy { crewId: SortOrder equipmentId: SortOrder date: SortOrder _count: RouteReportCountOrderBy _sum: RouteReportSumOrderBy _avg: RouteReportAvgOrderBy _min: RouteReportMinOrderBy _max: RouteReportMaxOrderBy count: RouteReportCountOrderBy @deprecated(reason: "Use _count") sum: RouteReportSumOrderBy @deprecated(reason: "Use _sum") avg: RouteReportAvgOrderBy @deprecated(reason: "Use _avg") min: RouteReportMinOrderBy @deprecated(reason: "Use _min") max: RouteReportMaxOrderBy @deprecated(reason: "Use _max") } input RouteReportCountOrderBy { id: SortOrder } input RouteReportSumOrderBy { totalMiles: SortOrder totalCost: SortOrder totalDriveSeconds: SortOrder totalStops: SortOrder } input RouteReportAvgOrderBy { totalMiles: SortOrder totalCost: SortOrder totalDriveSeconds: SortOrder totalStops: SortOrder } input RouteReportMinOrderBy { totalMiles: SortOrder totalCost: SortOrder totalDriveSeconds: SortOrder totalStops: SortOrder date: SortOrder } input RouteReportMaxOrderBy { totalMiles: SortOrder totalCost: SortOrder totalDriveSeconds: SortOrder totalStops: SortOrder date: SortOrder } type RouteReportCountAggregate { """Total number of routes in this group.""" id: SafeInt! } type RouteReportSumAggregate { """Total miles driven across all routes in this group.""" totalMiles: Decimal """Total estimated fuel cost across all routes in this group.""" totalCost: Decimal """Total windshield time in seconds across all routes in this group.""" totalDriveSeconds: SafeInt """Total number of route stops across all routes in this group.""" totalStops: SafeInt } type RouteReportAvgAggregate { """Average miles per route.""" totalMiles: Float """Average estimated fuel cost per route.""" totalCost: Float """Average windshield time in seconds per route.""" totalDriveSeconds: Float """Average number of stops per route.""" totalStops: Float } type RouteReportMinAggregate { totalMiles: Decimal totalCost: Decimal totalDriveSeconds: SafeInt totalStops: SafeInt date: LocalDate } type RouteReportMaxAggregate { totalMiles: Decimal totalCost: Decimal totalDriveSeconds: SafeInt totalStops: SafeInt date: LocalDate } """ A single row in a route report. Each row represents one group determined by the selected dimensions (crewId, equipmentId, date). If none of those fields are selected the query returns a single aggregate row covering all routes. """ type RouteReportRow { crewId: SafeInt equipmentId: SafeInt """ The route date for this group. Null when grouping across multiple dates. """ date: LocalDate """ Crew for this group. Null when crewId is not selected or no crew is set. """ crew: Crew """ Equipment (vehicle) for this group. Null when equipmentId is not selected. """ equipment: Equipment _count: RouteReportCountAggregate _sum: RouteReportSumAggregate _avg: RouteReportAvgAggregate _min: RouteReportMinAggregate _max: RouteReportMaxAggregate count: RouteReportCountAggregate @deprecated(reason: "Use _count") sum: RouteReportSumAggregate @deprecated(reason: "Use _sum") avg: RouteReportAvgAggregate @deprecated(reason: "Use _avg") min: RouteReportMinAggregate @deprecated(reason: "Use _min") max: RouteReportMaxAggregate @deprecated(reason: "Use _max") } """ Whether to use live or predicted traffic when fetching route traffic data. """ enum TrafficMode { """Current live conditions. Short cache (~2 min).""" REALTIME """ Predicted conditions at the route's scheduled departure time. HERE prices each leg at its own departure time, advancing the clock by stop dwell — so leg 5 of an 8am route is priced at ~11am, not 8am. Longer cache (~30 min). """ PREDICTED } """Congestion level for a road segment.""" enum CongestionLevel { UNKNOWN LOW MODERATE HEAVY SEVERE } """Traffic data for a single road segment within a leg.""" type SegmentTraffic { """Derived from trafficSpeed / baseSpeed ratio for map rendering.""" congestion: CongestionLevel """Current traffic speed in kph (converted from HERE m/s).""" trafficSpeedKph: Float """Free-flow speed in kph (converted from HERE baseSpeed m/s).""" baseSpeedKph: Float } """Traffic data for one leg of a route.""" type LegTraffic { """ Encoded polyline (Google, precision 5) of the road HERE routed for this leg in this traffic request. Draw segments on this line, not on Route.geometry: the stored route was routed separately and may follow a different road. Null when HERE returned no usable polyline. """ geometry: String """ One value per segment of `geometry` (length === geometry coordinate count - 1). Null when no traffic data was returned for this leg. """ segments: [SegmentTraffic] """ Traffic delay for this leg's drive in seconds (HERE duration - baseDuration, stop time excluded), floored at 0. Null when unavailable. """ delaySeconds: Int } """Per-segment traffic data for a route, with the geometry it applies to.""" type RouteTraffic { """ One entry per route leg, aligned 1:1 with Route.directions.legs by index. """ legs: [LegTraffic!]! } """A resolved coordinate with optional human-readable address.""" type Coordinate { longitude: Float! latitude: Float! """ Human-readable address for this location, if one was provided when the route was saved. """ address: String } type Route { id: SafeInt! """ Opaque key shared by every route in the same recurring series. Null for one-off (non-series) routes. """ seriesKey: String crewId: SafeInt equipmentId: SafeInt costPerGallon: Decimal milesPerGallon: Decimal date: LocalDate """ Company-local departure time saved on the route, as `HH:mm:ss` — the value passed as `departAt` when the route was created or updated. Null when none was set explicitly; see `departAt` for the instant the route actually departs. """ departTime: LocalTime """ The UTC instant this route departs the depot. Resolved the same way the routing provider and the arrival estimates resolve it: the saved `departTime` when there is one, else the earliest scheduled visit on the route, else 08:00 company-local. Null only when the route has no date. This is the timestamp; `departTime` is the wall clock it came from. """ departAt: DateTime totalMiles: Decimal """Total route distance in meters (derived from totalMiles).""" totalMeters: Int totalCost: Decimal totalTime: String """ Total windshield time in seconds — every stop's drive leg, plus the return to the depot when the end is not open. Null if the routing provider was unavailable when the route was saved. """ totalDriveSeconds: Int """ Total planned on-site work time in hours — sum of budgetedHours across all route stops. Zero if no stops have budgeted hours set. Use alongside totalDriveSeconds to compute the windshield-to-on-site ratio. """ budgetedHours: Float! """Number of stops — resolved via live Prisma count, always accurate.""" routeStopCount: Int! routeStops: [RouteStop!]! """ Crew assigned to this route. Null if crewId is unset or crew is not selected. """ crew: Crew """ Equipment (vehicle) assigned to this route. Null if equipmentId is unset or equipment is not selected. """ equipment: Equipment """ Turn-by-turn directions for this route in stop order. With both depots: depot → stops → depot. With an open start and/or end, the corresponding depot legs are omitted. Null when the route has no stops with coordinates, or if the directions API is unavailable. """ directions: DirectionsResult """ Directions leg for the return trip from the last stop back to the depot. Null when the end is open (end.type is NONE), when no return leg data has been saved, or when the route has no stops. """ returnLeg: DirectionsLeg """How this route starts, and the coordinates used for that end.""" start: RouteEndpoint! """How this route ends, and the coordinates used for that end.""" end: RouteEndpoint! """ Event id the dispatcher pinned as stop 1. Null when none is pinned. Always stop 1. Cleared when that event leaves the route (removed, deleted, or taken by another route). On recurring series create/update, remapped to each future date's matching visit instance. """ pinnedFirstEventId: SafeInt """ Per-segment traffic data for this route. REALTIME: current live conditions (Redis cache ~2 min). PREDICTED: predicted at the route's scheduled departAt, with per-leg departure times accounting for stop dwell times. Only available for saved routes; null for preview routes. Always computed with HERE, whatever the default routing provider is: only HERE advances each leg's departure time by the time spent at each stop. Null when no HERE API key is configured. traffic.legs[i] covers the same stops as Route.directions.legs[i]. segments[j] covers the road between traffic.legs[i].geometry coordinate j and j+1. """ traffic(mode: TrafficMode = REALTIME): RouteTraffic } """ One end of a route: how the depot was chosen, and the coordinates used. location is null when type is NONE, or when a COMPANY depot cannot be resolved. COMPANY is the company depot, resolved on read. CREW, USER, and CUSTOM are the snapshot stored on the route. userId is set only when type is USER. A CREW end uses the route's crewId. """ type RouteEndpoint { """How this end was chosen.""" type: RouteEndpointType! """Set when type is USER.""" userId: SafeInt """The employee in userId. Null otherwise, or when that user is gone.""" user: User """ Coordinates for this end. Null when type is NONE, or when a COMPANY depot cannot be resolved. """ location: Coordinate } type RouteStop { id: SafeInt! routeId: SafeInt! eventId: SafeInt! """The route this stop belongs to. Only fetched when selected.""" route: Route stopOrder: Int! routeDate: LocalDate! """ Drive time in seconds from the previous stop (or depot start for stop 0). Null if directions API data unavailable. """ driveTimeSeconds: Int """ Drive distance in meters from the previous stop (or depot start for stop 0). Null if directions API data unavailable. """ driveDistanceMeters: Int """Drive distance in miles. Null if driveDistanceMeters is null.""" driveDistanceMiles: Float """ Estimated clock arrival at this stop. Only meaningful when departAt was known at save time. """ estimatedArrivalAt: DateTime """ Estimated departure from this stop (estimatedArrivalAt + event duration). Null if duration unknown. """ estimatedDepartureAt: DateTime event: Event """ Full directions leg for the drive from the previous stop (or depot) to this stop. Composed from cached DB columns (rs_leg_steps, driveDistanceMeters, driveTimeSeconds). Null if directions data has not been saved for this route. """ leg: DirectionsLeg } """ Controls whether the assignment (crew or individual employees) inferred from this route's events is propagated to future route occurrences in the series. Only applies when repeatEveryWeeks is set. Defaults to OFF. OFF – assignment is not propagated; future routes infer their own SOFT – propagate to future occurrences, skipping any that already have an assignment (crew or employees) OVERRIDE – propagate to all future occurrences, replacing any existing assignments """ enum RepeatAssignmentOption { OFF SOFT OVERRIDE } """ Routing provider to use for a request. Absent means the provider this deployment is configured with. Routing results are per-request and cached per provider, so choosing one here changes nothing that other requests can see. Geocoding is deliberately not selectable: it writes coordinates back to shared property and company rows. """ enum RoutingProvider { MAPBOX HERE } """ How a route's start or end depot is chosen. COMPANY — use the company's configured route start/end (historical default). CREW — use the route's crew location for that end. The crew is the route's crewId; no separate crew id is stored. Coordinates are snapshotted at save. USER — use a specific employee's on-file address. Requires startUserId or endUserId. Coordinates are snapshotted at save and do not follow later address changes until this type is sent again. CUSTOM — use the coordinate supplied in startLocation / endLocation. address on that coordinate is the display label (typed address or map pin). NONE — no depot; the route is open at that end (starts at the first stop / ends at the last, with no return leg). """ enum RouteEndpointType { COMPANY CREW USER CUSTOM NONE } """ Result of a deleteRoute mutation — how many routes were deleted (0 or 1 for a single route, or more when andFutureRoutesInSeries is used). """ type DeleteRouteResult { deletedCount: Int! } """Input for createRoute.""" input CreateRouteInput { """The route date (YYYY-MM-DD in the company's timezone)""" date: LocalDate! """ Event IDs in the order they should be visited (stopOrder 1 upward). Pass `optimize: true` on the mutation to have the provider choose the order instead. Every id must belong to the company (unknown ids are a 404). Each id may appear once, the event must not be deleted, and it must be scheduled on `date` — its start date, or any day through its end date for a multi-day event; otherwise the request is a 400 naming the offending ids. """ eventIds: [SafeInt!]! """ Crew to assign to this route (and its events). Omit to derive from the majority crew of eventIds; null to explicitly clear. """ crewId: SafeInt """ Departure time (HH:MM:SS, company-local). Omit to derive from the earliest event start time. """ departAt: LocalTime """ Depot start location. Required when startLocationType is CUSTOM; ignored otherwise. """ startLocation: CoordinateInput """ Depot end location. Required when endLocationType is CUSTOM; ignored otherwise. """ endLocation: CoordinateInput """ How the start depot is chosen. Omit for COMPANY (use company settings). Pass NONE for an open start (route begins at the first stop). Pass CUSTOM with startLocation for a custom coordinate. Pass USER with startUserId, or CREW with crewId. """ startLocationType: RouteEndpointType """ How the end depot is chosen. Omit for COMPANY (use company settings). Pass NONE for an open end (no return leg). Pass CUSTOM with endLocation for a custom coordinate. Pass USER with endUserId, or CREW with crewId. """ endLocationType: RouteEndpointType """ Employee whose on-file address is the start. Required when startLocationType is USER. """ startUserId: SafeInt """ Employee whose on-file address is the end. Required when endLocationType is USER. """ endUserId: SafeInt """ Event id to pin as stop 1. Must be one of eventIds. Null/omit means no pin. The pinned event is stop 1 with or without optimize, even if it is listed elsewhere in eventIds. Persisted on the route so later optimizeRoute / Dispatch Board reloads honor it. """ pinnedFirstEventId: SafeInt } """ Input for updateRoute. All fields except eventIds are optional — omitted fields keep their current values. """ input UpdateRouteInput { """ New ordered list of event IDs. Replaces the current stop list outright, so an event left out of the list is removed from the route. Omit the field entirely to keep the stops as they are. """ eventIds: [SafeInt!] """Crew override. Omit to keep existing; null to explicitly clear.""" crewId: SafeInt """ Departure time override (HH:MM:SS, company-local). Omit to keep existing; null to clear (derive from event times). """ departAt: LocalTime """ Depot start location override. Omit to keep existing; null to clear to COMPANY (and clears any custom coordinate). Required when startLocationType is set to CUSTOM. """ startLocation: CoordinateInput """ Depot end location override. Omit to keep existing; null to clear to COMPANY (and clears any custom coordinate). Required when endLocationType is set to CUSTOM. """ endLocation: CoordinateInput """ How the start depot is chosen. Omit to keep the existing type. Setting this to NONE opens the start; COMPANY restores company settings; CUSTOM requires startLocation. USER requires startUserId and re-resolves that employee's address. CREW re-resolves the route crew's start. """ startLocationType: RouteEndpointType """ How the end depot is chosen. Omit to keep the existing type. Setting this to NONE opens the end; COMPANY restores company settings; CUSTOM requires endLocation. USER requires endUserId and re-resolves that employee's address. CREW re-resolves the route crew's end. """ endLocationType: RouteEndpointType """ Employee for a USER start. Required when startLocationType is set to USER. Omit to keep the current user. """ startUserId: SafeInt """ Employee for a USER end. Required when endLocationType is set to USER. Omit to keep the current user. """ endUserId: SafeInt """ Event id to pin as stop 1. Must be one of the effective eventIds. Omit to keep existing; null to clear. The pinned event is stop 1 with or without optimize, even if it is listed elsewhere in eventIds, so unpin it (null) to move it. Auto-cleared if the pinned event is removed from the stop list or taken by another route. """ pinnedFirstEventId: SafeInt } input RouteFilter { id: IntFilter crewId: IntFilter equipmentId: IntFilter date: DateFilter stops: RouteStopFilter AND: [RouteFilter!] OR: [RouteFilter!] NOT: RouteFilter } input RouteSort { id: SortOrder date: SortOrder } input RouteStopFilter { id: IntFilter routeId: IntFilter eventId: IntFilter routeDate: DateFilter event: EventFilter AND: [RouteStopFilter!] OR: [RouteStopFilter!] NOT: RouteStopFilter } input RouteStopSort { id: SortOrder stopOrder: SortOrder routeDate: SortOrder } """A [longitude, latitude] coordinate pair.""" input CoordinateInput { longitude: Float! latitude: Float! """ Optional label (e.g. event ID) echoed back on the matching leg, so a caller can map results to its own records. Never sent to the routing provider. """ label: String """ Human-readable address for this location (stored so it can be reloaded without reverse-geocoding). """ address: String } """Drive time and distance for an ordered set of waypoints.""" type DirectionsResult { """Total route distance in meters.""" totalMeters: Int! """Total route distance in miles.""" totalMiles: Float! """Total route duration in seconds.""" totalDurationSeconds: Int! """Total route duration formatted as HH:MM.""" totalTime: String! """ Estimated fuel cost (totalMiles * costPerGallon / milesPerGallon). Null if not provided. """ totalCost: Float """ Detailed breakdown of the route into legs. Each leg represents the journey between two consecutive waypoints (e.g., from the depot to the first stop, or between two stops). """ legs: [DirectionsLeg!]! """ Full route geometry as an encoded polyline (Google polyline format, precision 5). Decode with e.g. @mapbox/polyline. Null when unavailable. """ geometry: String } """A turn-by-turn maneuver step within a route leg.""" type DirectionsStep { """Human-readable navigation instruction.""" instruction: String! """ Maneuver type (e.g. 'turn', 'depart', 'arrive', 'merge', 'roundabout'). """ maneuverType: String! """ Maneuver modifier (e.g. 'right', 'left', 'slight right'). Null for depart/arrive. """ maneuverModifier: String """Step distance in meters.""" distanceMeters: Int! """Step duration in seconds.""" durationSeconds: Int! """ Step geometry as an encoded polyline (Google polyline format, precision 5). Null when unavailable. """ geometry: String } """A leg of a route between two consecutive waypoints.""" type DirectionsLeg { """ Label of the waypoint this leg departs from (from the input coordinates label, if provided). """ startLabel: String """ Label of the waypoint this leg arrives at (from the input coordinates label, if provided). """ endLabel: String """Leg distance in meters.""" distanceMeters: Int! """Leg distance in miles.""" distanceMiles: Float! """Leg duration in seconds.""" durationSeconds: Int! """ Detailed turn-by-turn navigation instructions (maneuvers) for this leg. """ steps: [DirectionsStep!]! } """ Result of an optimize route call — events in the optimized order, plus route metrics taken from a directions call rather than the optimizer. """ type OptimizeRouteResult { """ Events in the optimized order. Events without resolvable coordinates are excluded. """ orderedEvents: [Event!]! """Total route distance in meters.""" totalMeters: Int! """Total route distance in miles.""" totalMiles: Float! """Total route duration in seconds.""" totalDurationSeconds: Int! """Total route duration formatted as HH:MM.""" totalTime: String! """ Detailed breakdown of the route into legs, including turn-by-turn maneuvers. Empty array if directions call fails or if there are fewer than 2 waypoints. """ legs: [DirectionsLeg!]! """ Full optimized route geometry as an encoded polyline (Google polyline format, precision 5). Null if unavailable. """ geometry: String } """ Nearest event on the same day, ranked by drive time (or crow-flies in fastMode). """ type NearbyEvent { eventId: SafeInt! """ Null if the resolver only loaded a partial Event select and the full relation was not fetched. """ event: Event """Crow-flies distance in meters.""" distanceMeters: Int! """Crow-flies distance in miles.""" distanceMiles: Float! """Actual drive time in seconds. Null in fastMode or on API failure.""" driveTimeSeconds: Int """Actual drive distance in meters. Null in fastMode or on API failure.""" driveDistanceMeters: Int """Actual drive distance in miles. Null in fastMode or on API failure.""" driveDistanceMiles: Float """ The route stop for this event on this day, if it's already assigned to a route. Null if unrouted. """ routeStop: RouteStop } """Cheapest insertion point for a new stop on an existing route.""" type RouteInsertionSuggestion { """The ID of the route this insertion suggestion is for.""" routeId: SafeInt! route: Route """ The 0-based index of the existing stop after which the new stop should be inserted. 0 means it should be inserted at the very beginning (before stop 1), and a value equal to the current stop count means it should be appended at the end. """ insertAfterStop: Int! """ The additional time (in seconds) added to the total route duration by inserting this new stop. """ extraSeconds: Int! """ The additional distance (in meters) added to the total route distance by inserting this new stop. """ extraMeters: Int! """Additional miles.""" extraMiles: Float! """ Directions for just the affected segment: the stop before the insertion point, the new location, and the stop after (1–2 legs). Only fetched when selected. Null if surrounding stops lack coordinates or directions API is unavailable. """ patchDirections: DirectionsResult } """Result container for the routeSuggestions query.""" type RouteInsertionSuggestions { """ The top 3 nearest events on the same day, ranked by drive time (or crow-flies distance if fastMode is enabled). """ nearbyEvents: [NearbyEvent!]! """Per-route insertion cost, ranked by extraSeconds ascending.""" routes: [RouteInsertionSuggestion!]! } """A single day ranked by cost of inserting a new stop.""" type SchedulingDateSuggestion { """The date being evaluated.""" date: LocalDate! """Per-route insertion costs for this day. Empty if no routes exist.""" routes: [RouteInsertionSuggestion!]! """ The nearest events on this day (with or without existing routes), ranked by drive time from the candidate location (or crow-flies distance if fastMode is enabled). """ nearbyEvents: [NearbyEvent!]! """ The additional time (in seconds) required to 'thread' the candidate location between the two most optimal nearby events on this day. Calculated as the marginal cost: driveTime(Event1 to Location) + driveTime(Location to Event2) - driveTime(Event1 to Event2). Null if fewer than 2 nearby events have coordinates. """ pairInsertionSeconds: Int """ The additional distance (in meters) required to 'thread' the candidate location between the two most optimal nearby events. Null if fewer than 2 nearby events have coordinates. """ pairInsertionMeters: Int """pairInsertionMeters in miles.""" pairInsertionMiles: Float """ Directions between the location and the two nearest events on this day (location → nearest → 2nd nearest). Only fetched when selected. Null if fewer than one nearby event has coords or directions API is unavailable. """ patchDirections: DirectionsResult """ Transit cost from company depot to this location (baseline for a new solo route). """ depotSeconds: Int! """Depot-to-location distance in meters.""" depotMeters: Int! """depotMeters in miles.""" depotMiles: Float! } type RoutePreview { crewId: SafeInt equipmentId: SafeInt date: LocalDate departAt: DateTime totalMiles: Decimal totalMeters: Int totalCost: Decimal totalDriveSeconds: Int budgetedHours: Float! routeStopCount: Int! routeStops: [RoutePreviewStop!]! directions: DirectionsResult returnLeg: DirectionsLeg """ How this preview starts, and the coordinates that would be used for that end. """ start: RouteEndpoint! """ How this preview ends, and the coordinates that would be used for that end. """ end: RouteEndpoint! } type RoutePreviewStop { eventId: SafeInt! stopOrder: Int! driveTimeSeconds: Int driveDistanceMeters: Int driveDistanceMiles: Float estimatedArrivalAt: DateTime estimatedDepartureAt: DateTime leg: DirectionsLeg """ The event for this stop. Loaded per stop (N+1 acceptable — preview calls are rare and bounded to ~20 stops). Null if eventId is missing or the event is not found. """ event: Event } type Sms { id: SafeInt! customerId: SafeInt customer: Customer counterPartyUserId: SafeInt counterPartyUser: User sentByUserId: SafeInt sentByUser: User pcsId: SafeInt! sid: String! """The phone number this message was sent to""" to: String! """The phone number this message was sent from""" from: String! """Message body""" text: String! notes: String status: SmsStatus! direction: SmsDirection! segments: SafeInt! isViewed: Boolean! isDone: Boolean! stripeStatus: SmsStripeStatus! """ Canonical conversation identifier — sorted concat of to/from phone numbers """ conversationId: String """The other party's phone number (from for inbound, to for outbound)""" counterparty: String createdAt: DateTime! updatedAt: DateTime! sentAt: DateTime! } input SmsFilter { id: IntFilter customerId: IntFilter counterPartyUserId: IntFilter counterPartyUser: UserFilter sentByUserId: IntFilter sentByUser: UserFilter direction: SmsDirection status: SmsStatusFilter stripeStatus: SmsStripeStatusFilter isViewed: Boolean isDone: Boolean createdAt: DateTimeFilter sentAt: DateTimeFilter conversationId: String counterparty: StringFilter AND: [SmsFilter!] OR: [SmsFilter!] NOT: SmsFilter } input SmsSort { id: SortOrder createdAt: SortOrder sentAt: SortOrder customerId: SortOrder direction: SortOrder } type SmsTemplate { id: SafeInt! subject: String! text: String! } """Why the customer cancelled their subscription.""" input CancellationFeedbackInput { cancellationReason: CancellationReason! cancellationReasonCustom: String highlightEaseOfUse: Boolean highlightCustomerSupport: Boolean highlightGreatCommunity: Boolean highlightValueForPrice: Boolean highlightGreatOnboarding: Boolean highlightOther: Boolean satisfactionHighlightCustom: String } type CompanyStripeSetupIntent { clientSecret: String! setupIntentId: String! customerSessionClientSecret: String! } input BillingAddressInput { postalCode: String! country: String! } type StripeSubscription { subscriptionId: String! status: String! currentPeriodEnd: DateTime! } type StripeSubscriptionPhase { tier: Tier! planPriceId: String! planFrequency: BillingInterval! additionalUsers: SafeInt! startDate: DateTime! endDate: DateTime! } """ Result of the deprecated retrySubscriptionPayment. Use resumeSubscription, which returns the company and fails with typed errors instead. """ type RetryPaymentResult { success: Boolean! """No longer filled in; always null.""" invoiceId: String error: String declineCode: String } """How long a promo code's discount applies.""" enum PromoCodeDuration { """Applies to the first invoice only.""" ONCE """Applies for a fixed number of months (see durationInMonths).""" REPEATING """Applies to every invoice for the life of the subscription.""" FOREVER } """ Details of the discount a promo code applies, for display during checkout. """ type PromoCodeDetails { """ Normalized customer-facing code as stored in Stripe (canonical casing). """ code: String! """ Human-readable summary of the discount (e.g. 'First month free', '20% off for 3 months'). """ description: String! """ Percent off the subtotal for percentage codes. Null for fixed-amount codes. """ percentOff: Float """ Fixed amount off in cents for fixed-amount codes. Null for percentage codes. """ amountOffCents: SafeInt """ Uppercase ISO currency for amountOffCents (e.g. 'USD'). Null for percentage codes. """ currency: String """ Whether the discount applies once, repeats for a number of months, or forever. """ duration: PromoCodeDuration! """ Number of months the discount repeats when duration is REPEATING. Null otherwise. """ durationInMonths: Int } """ Why a promo code could not be applied at checkout (set only when isValid is false). """ enum PromoCodeErrorReason { """The code is blank, unknown, or no longer active.""" INVALID """ The code is real but can't be redeemed for this account or plan (e.g. first-time-customers-only, usage limit, minimum amount). """ NOT_APPLICABLE """ Validation couldn't complete (Stripe unreachable, or no checkout context yet); the code may still be valid on retry. """ UNAVAILABLE } """ Why a promo code could not be applied during checkout. Present only when isValid is false. """ type PromoCodeError { """Machine-readable failure reason for branching.""" reason: PromoCodeErrorReason! """Short, customer-facing explanation for display.""" message: String! } """Result of validating a promo code against a plan during checkout.""" type PromoCodeValidation { """ Whether the promo code is valid and can be applied to the selected plan. """ isValid: Boolean! """ First-invoice total in cents before the promo code is applied. Null when the code is invalid. """ originalTotalCents: SafeInt """ First-invoice total in cents after the promo code is applied (e.g. 0 for a one-month-free referral code). Null when the code is invalid. NOTE: reflects the first invoice only — for REPEATING/FOREVER discounts use details.duration to convey that the discount recurs. """ discountedTotalCents: SafeInt """ Discount details for display (percentage/amount, duration, description). Present only when isValid is true. """ details: PromoCodeDetails """ Failure reason and display message. Present only when isValid is false. """ error: PromoCodeError } enum TagObjectType { ASSET CREW CUSTOMER EVENT RECURRING_EVENT VENDOR } type Tag { id: SafeInt! tag: String! createdAt: DateTime updatedAt: DateTime objectTagCount(type: TagObjectType): SafeInt! """ True if this tag is attached to at least one event (any status). hasEvents, hasCustomers, and hasRecurringEvents share a single batched EXISTS-style query per request — selecting all three costs the same as one. """ hasEvents: Boolean! """True if this tag is attached to at least one customer.""" hasCustomers: Boolean! """True if this tag is attached to at least one recurring event.""" hasRecurringEvents: Boolean! eventCount(where: EventFilter): SafeInt! events(where: EventFilter, orderBy: [EventSort!], take: SafeInt, skip: SafeInt): [Event!]! customerCount(where: CustomerFilter): SafeInt! customers(where: CustomerFilter, orderBy: [CustomerSort!], take: SafeInt, skip: SafeInt): [Customer!]! recurringEventCount(where: RecurringEventFilter): SafeInt! recurringEvents(where: RecurringEventFilter, orderBy: [RecurringEventSort!], take: SafeInt, skip: SafeInt): [RecurringEvent!]! } input TagFilter { tag: StringFilter """Filter to tags that have at least one object tag of the given type.""" hasEvents: Boolean """Filter to tags that are attached to at least one customer.""" hasCustomers: Boolean """Filter to tags that are attached to at least one recurring event.""" hasRecurringEvents: Boolean } input TagOrderBy { id: SortOrder tag: SortOrder createdAt: SortOrder updatedAt: SortOrder objectTagCount: SortOrder } enum PropertyDataSet { SAN_FRANCISCO_USA CHICAGO_USA PORTLAND_USA LONDON_UK } type TimeEntryReportRow { userId: SafeInt categoryId: SafeInt eventId: SafeInt customerId: SafeInt propertyId: SafeInt user: User category: TimeEntryCategory event: Event customer: Customer property: Property _count: TimeEntryReportCountAggregate _sum: TimeEntryReportSumAggregate _avg: TimeEntryReportAvgAggregate _min: TimeEntryReportMinAggregate _max: TimeEntryReportMaxAggregate count: TimeEntryReportCountAggregate @deprecated(reason: "Use _count") sum: TimeEntryReportSumAggregate @deprecated(reason: "Use _sum") avg: TimeEntryReportAvgAggregate @deprecated(reason: "Use _avg") min: TimeEntryReportMinAggregate @deprecated(reason: "Use _min") max: TimeEntryReportMaxAggregate @deprecated(reason: "Use _max") } enum TimeEntryType { GLOBAL_TIMER EVENT_TIMER } type TimeEntryCategory { id: SafeInt! name: String! color: String! isDeletable: Boolean! } input TimeEntryCategorySort { name: SortOrder id: SortOrder } type TimeEntry { id: SafeInt! type: TimeEntryType! userId: SafeInt! user: User! """Not null for global timers. Null for event timers.""" categoryId: SafeInt category: TimeEntryCategory """Not null for event timers. Null for global timers.""" eventId: SafeInt event: Event """Optional input for event timers""" customerId: SafeInt customer: Customer """Optional input for event timers""" propertyId: SafeInt property: Property reviewerUserId: SafeInt paymentId: SafeInt bonusPaymentId: SafeInt """Timestamp that timer started""" startedAt: DateTime! """Timestamp that timer ended""" endedAt: DateTime """Total seconds spent between start and end times""" totalSeconds: SafeInt! acceptedAt: DateTime deniedAt: DateTime hourlyRate: Decimal paymentAmount: Decimal bonusPaymentAmount: Decimal! location: Location notes: String } enum ClockInOutType { CLOCK_IN CLOCK_OUT } type ClockInOutAction { id: SafeInt! type: ClockInOutType! userId: SafeInt! user: User! """UTC timestamp that the user clocked in""" createdAt: DateTime! location: Location } input TimeEntryInput { """Not null for global timers. Null for event timers.""" categoryId: SafeInt """Not null for event timers. Null for global timers.""" eventId: SafeInt """Optional input for event timers""" customerId: SafeInt """Optional input for event timers""" propertyId: SafeInt """Timestamp that timer started""" startedAt: DateTime! """Timestamp that timer ended""" endedAt: DateTime! location: LocationInput notes: String userId: SafeInt hourlyRate: Decimal } input UpdateTimeEntryInput { """Timestamp that timer started""" startedAt: DateTime """Timestamp that timer ended""" endedAt: DateTime notes: String hourlyRate: Decimal } input TimeEntrySort { timeStart: SortOrder timeEnd: SortOrder id: SortOrder } input TimeEntryFilter { id: IntFilter userId: IntFilter categoryId: IntFilter eventId: IntFilter customerId: IntFilter propertyId: IntFilter reviewerUserId: IntFilter paymentId: IntFilter bonusPaymentId: IntFilter startedAt: DateTimeFilter endedAt: DateTimeFilter totalSeconds: IntFilter acceptedAt: DateTimeFilter deniedAt: DateTimeFilter hourlyRate: DecimalFilter paymentAmount: DecimalFilter bonusPaymentAmount: DecimalFilter notes: String user: UserFilter isRunning: Boolean } enum MockTwilioError { PN_NOT_PROVIDED PN_NOT_WITH_ISV PN_NOT_TOLLFREE PN_STATUS_IS_NOT_IN_USE NOTIFICATION_EMAIL_NOT_PROVIDED VERIFICATION_NOT_RESUBMITTABLE TRIAL_ACCOUNT GENERIC } """ https://www.twilio.com/docs/messaging/compliance/toll-free/compliance-embeddable-onboarding#initialize-complianceinquiry-api-for-toll-free-verification """ type ComplianceInquiryResponse { data: TwilioComplianceInquiry error: ComplianceInquiryError } type TwilioComplianceInquiry { id: SafeInt! companyId: SafeInt! subAccountSid: String! phoneNumber: String! """ A reference for your Toll-free verification Inquiry in Twilio. This value will be used to initialize the client component. """ inquiryId: String """ An ephemeral session token for your Compliance Inquiry in Twilio. This value will be used to initialize the client component. Expires in 1440 minutes from creation. """ inquirySessionToken: String sessionTokenUpdated: DateTime """A reference for your Toll-free verification resource in Twilio.""" registrationId: String url: String createdAt: DateTime updatedAt: DateTime } type ComplianceInquiryError { status: Int! message: String! code: Int moreInfo: String } """ See https://www.twilio.com/docs/messaging/compliance/toll-free/api-onboarding#create-a-tfv-request """ type TwilioVerification { sid: String accountSid: String customerProfileSid: String trustProductSid: String dateCreated: DateTime dateUpdated: DateTime regulatedItemSid: String businessName: String businessStreetAddress: String businessStreetAddress2: String businessCity: String businessStateProvinceRegion: String businessPostalCode: String businessCountry: String businessWebsite: String businessContactFirstName: String businessContactLastName: String businessContactEmail: String businessContactPhone: String notificationEmail: String useCaseCategories: [String] useCaseSummary: String productionMessageSample: String optInImageUrls: [String] optInType: String messageVolume: String additionalInformation: String tollfreePhoneNumberSid: String tollfreePhoneNumber: String status: String url: String rejectionReason: String rejectionReasons: [TwilioRejectionReason] errorCode: Int editExpiration: DateTime editAllowed: Boolean businessRegistrationNumber: String businessRegistrationAuthority: String businessRegistrationCountry: String businessType: String businessRegistrationPhoneNumber: String doingBusinessAs: String optInConfirmationMessage: String helpMessageSample: String privacyPolicyUrl: String termsAndConditionsUrl: String ageGatedContent: String optInKeywords: String } type TwilioRejectionReason { code: Int reason: String } type Upsell { id: SafeInt! companyId: SafeInt! name: String! emailTemplate: SafeInt! textTemplate: SafeInt! emailActive: Boolean! textActive: Boolean! createdAt: DateTime updatedAt: DateTime reviewRequired: Boolean! } type UpsellHistory { id: SafeInt! upsellId: SafeInt! companyId: SafeInt! customerId: SafeInt! senderUserId: SafeInt! eventId: SafeInt! sentTime: BigInt! name: String! wasSent: Boolean! upsell: Upsell customer: Customer senderUser: User event: Event } input UpsellFilter { id: IntFilter name: StringFilter reviewRequired: Boolean emailActive: Boolean textActive: Boolean } input UpsellSort { id: SortOrder name: SortOrder createdAt: SortOrder } input SubmitUpsellInput { upsellId: SafeInt! eventId: SafeInt! customerId: SafeInt! } type UserDayOverride { id: SafeInt! userId: SafeInt! date: Date! """ Available hours for this user on this date. Set to 0 to mark the user as unavailable. """ capacityHours: Decimal! createdAt: DateTime! updatedAt: DateTime! } input UserDayOverrideFilter { id: IntFilter userId: IntFilter date: DateFilter capacityHours: DecimalFilter AND: [UserDayOverrideFilter!] OR: [UserDayOverrideFilter!] NOT: UserDayOverrideFilter } input UserDayOverrideSort { id: SortOrder userId: SortOrder date: SortOrder capacityHours: SortOrder } input ScheduleColorSettingInput { color: String contrast: Boolean } """ Per-event-type color and contrast settings for the schedule view, merged onto the stored value. An entry left unset is left unchanged; passing an entry as `null` reverts that event type's color and contrast to their defaults. """ input ScheduleColorSettingsInput { visit: ScheduleColorSettingInput todo: ScheduleColorSettingInput meeting: ScheduleColorSettingInput maintenance: ScheduleColorSettingInput } """ Legacy color-only input. Accepted but ignored; remove once no client sends it. """ input ScheduleColorsInput { visit: String todo: String meeting: String maintenance: String } input SetScheduleColorsInput { scheduleColorSettings: ScheduleColorSettingsInput! scheduleColors: ScheduleColorsInput @deprecated(reason: "Ignored. Use scheduleColorSettings.") } input CreateUserInput { email: String! password: String! firstName: String! lastName: String! role: UserRole = EMPLOYEE hireDate: Date birthDate: Date } """Company knowledge-base page (CoPilot Wiki).""" type Wiki { id: SafeInt! companyId: SafeInt! title: String! pageName: String! """HTML content of the wiki page.""" content: String isActive: Boolean! showOnMenu: Boolean! actionsEnabled: Boolean! """ Parent wiki id. Null for root pages (DB stores 0 as a legacy placeholder). """ parentId: SafeInt parent: Wiki children: [Wiki!]! hasChildren: Boolean! authorUserId: SafeInt createdAt: DateTime! updatedAt: DateTime } input WikiFilter { id: IntFilter title: StringFilter pageName: StringFilter content: StringFilter isActive: Boolean showOnMenu: Boolean parentId: IntFilter parent: WikiFilter } input WikiSort { id: SortOrder title: SortOrder pageName: SortOrder createdAt: SortOrder updatedAt: SortOrder } """ Tax rate breakdown returned by the ZipTax integration. All rates are expressed as percentages (e.g. 8.25, not 0.0825). """ type TaxRateLookupResult { """County tax rate as a percentage""" county: Decimal! """Combined city + district tax rate as a percentage""" city: Decimal! """State tax rate as a percentage""" state: Decimal! """Total combined tax rate (county + city + state) as a percentage""" totalTax: Decimal! """ZipTax response code — 100 = success, 110 = no results, etc.""" statusCode: Int """Human-readable message corresponding to statusCode""" statusMessage: String """ True when the result was served from the DB cache (no external API call made) """ fromCache: Boolean! }