Automated scheduling, instance tracking, and billing for repeat services. Poster creates schedules, Contractor manages visits.
RecurringJobCreate — form with frequency chips, contractor selector
createRecurringJob mutation, useContractorHistory hook
RecurringAgreementDetail — schedule card, contractor, upcoming instances
useRecurringAgreement, pauseAgreement, upcomingInstances query
ContractorRecurringScreen — FlatList with segmented filter tabs
listForContractor query, SegmentedControl, AgreementCard component
RecurringInstanceScreen — checklist, notes, photo upload
instances.get query, confirmInstance, startWork, completeInstance mutations
CompleteInstanceScreen — photo grid, notes, time entry
completeInstance mutation, MediaUpload component, generateUploadUrl
RecurringHistoryScreen — timeline with status dots, filter chips
listForAgreement query (paginated), date range filter, InstanceRow component
RecurringBillingScreen — spending chart, service breakdown, auto-pay
billingForPoster query, Stripe PaymentMethod, listUpcomingCharges
ScheduleConflictScreen — conflict banner, resolution options, substitutes
skipInstance, rescheduleInstance mutations, findSubstitutes query
recurringServiceAgreements
title: v.string()
frequency: "weekly" | "biweekly" | "monthly" | "quarterly" | "custom"
preferredDayOfWeek: v.optional(v.number())
preferredTime: v.optional(v.string())
posterId: v.id("posterUsers")
contractorId: v.id("contractorUsers")
pricePerOccurrence: v.number()
status: "active" | "paused" | "completed" | "cancelled"
autoPayEnabled: v.boolean()
startDate / endDate: v.number()
recurringServiceInstances
agreementId: v.id("recurringServiceAgreements")
scheduledDate: v.number()
status: "scheduled" | "confirmed" | "in_progress" | "completed" | "skipped"
completionMedia: v.optional(v.array(...))
completionNotes: v.optional(v.string())
paymentStatus: v.optional(v.string())
useRecurringJobs
agreements.listForPoster({ status })
agreements.listForContractor({ status })
useRecurringInstances
instances.listUpcomingForPoster({ limit })
instances.listForAgreement({ agreementId })
instances.get({ instanceId })
agreements.get
Returns: agreement, contractor,
property, upcomingInstances,
recentInstances
createRecurringJob
title, frequency, dayOfWeek,
contractorId, price, autoPay
completeInstance
instanceId, completionNotes,
completionMedia[]
pauseSchedule / resume
agreementId, reason?
skipInstance
instanceId, reason
rescheduleInstance
instanceId, newDate, newTime
Auto-charge flow
1. Instance completed
2. Stripe PaymentIntent created
3. Charge saved payment method
4. Update instance paymentStatus
Fee structure
platform fee deducted based on subscription tier
40/60 payout split
(same as one-time jobs)
Conflict resolution
findSubstitutes query
assignSubstitute mutation