Developer Handover Pack
Everything needed to build the automation from scratch: current state, full agent specs, data flow, and the build stack.
Developer Handover Pack
Job / Project Scheduling Automation
[YourCompany.com] · Operations Department · Prepared by FullSpec · [Today's Date]
This document gives the FullSpec build team everything needed to implement the Job / Project Scheduling automation end to end. It covers the current manual process, all agent specifications with IO contracts, the full data flow from trigger to final output, and the recommended build stack. The process owner's responsibility is to confirm tool access and calendar hygiene before build begins. Everything else, including agent construction, integration wiring, testing, and handoff, is handled by the FullSpec team.
01Process snapshot
02Agent specifications
This agent is the core scheduling engine. It fires the moment a job status transitions to confirmed in Jobber, reads all job metadata from the webhook payload, and queries Google Calendar for each eligible crew member within the required date and time window. It ranks available crew by availability first, then by proximity rules if a location field is populated on the Jobber job record. Once the best match is identified it creates a Google Calendar event with the job address, customer contact details, scope notes, and all assigned crew as attendees, then writes the confirmed assignment back to the Jobber job record. If no crew member is free, it routes an exception flag to the scheduler and halts without creating a partial event. Estimated build time: 16 hours. Complexity: Moderate.
// Input (from Jobber webhook payload)
job_id : string // Jobber unique job identifier
job_status : string // must equal 'confirmed' to proceed
job_title : string // human-readable job name
required_date : date // ISO 8601 preferred start date
required_window : object // { start: datetime, end: datetime }
site_address : string // full service address
customer_name : string // contact name for calendar event
customer_phone : string // on-the-day contact number
scope_notes : string // free-text job description / special instructions
crew_required : integer // number of crew members needed
// Output (passed to Notifications and CRM Sync Agent)
assignment_id : string // internal run UUID for traceability
assigned_crew : array // [{ crew_member_id, name, email, calendar_id }]
calendar_event_id : string // Google Calendar event ID (created by this agent)
calendar_event_link : string // HTML link to the created event
confirmed_start : datetime // ISO 8601 confirmed job start
confirmed_end : datetime // ISO 8601 confirmed job end
jobber_job_id : string // echoed through for downstream sync
site_address : string // echoed through for notifications
customer_name : string // echoed through for customer email
customer_email : string // looked up from Jobber contact record
customer_phone : string // echoed through
scope_notes : string // echoed through
// On exception (no crew available)
exception_flag : boolean // true when no assignment could be made
exception_reason : string // human-readable explanation for scheduler
scheduler_alert_sent : boolean // true after Slack DM dispatched to scheduler- Jobber webhook version: use Jobber REST API v2. Register the webhook at POST /v2/webhooks targeting the automation platform's inbound URL. The webhook secret must be stored in the shared credential store and validated on every inbound request using HMAC-SHA256 signature verification.
- Google Calendar API tier: requires Google Workspace (Business Starter or above) with Calendar API v3 enabled in the connected Google Cloud project. The service account or OAuth client must have read access to all crew member calendars via domain-wide delegation or per-user consent before build begins.
- Crew calendar list: the list of crew Google Calendar IDs must be stored as a structured variable in the automation platform (not hardcoded). It must be updatable by the process owner without a code change.
- Availability query: call Events.list on each crew calendar with timeMin=required_window.start and timeMax=required_window.end, checking for zero overlapping events with status 'confirmed' or 'tentative'. A 'free/busy' query (Calendar.freebusy) is preferred for efficiency when checking more than three crew members simultaneously.
- Calendar event creation: the event must be created with status 'confirmed', visibility 'private', and attendees set to all assigned crew email addresses. The event description field must include job_id, site_address, scope_notes, and customer_phone.
- Jobber write-back: PATCH /v2/jobs/{job_id} to update assigned_to (crew member IDs) and scheduled_start / scheduled_end. Confirm the Jobber subscription tier supports job update via API before build begins; this requires Jobber Grow or Connect plan.
- Dedupe / idempotency: store assignment_id keyed on job_id in the error log table. If the same job_id is received twice (webhook retry), check the table first and skip creation if a calendar event already exists for that job_id.
- Exception routing: if no crew is available, send a Slack DM to the designated scheduler user ID (stored in credential store) with the job details and required window. Do not create a partial calendar event. Set exception_flag = true in the output payload.
- Proximity rules: if the Jobber job record includes a GPS-derived postcode or zone field, sort available crew by zone match before selecting. If no zone data is present, fall back to first-available ordering. Confirm whether zone data exists in Jobber before implementing proximity logic.
This agent takes the confirmed assignment output from the Availability and Assignment Agent and executes three parallel downstream actions: a Slack notification to the assigned crew, a Gmail confirmation email to the customer, and a HubSpot deal record update. It runs only when exception_flag is false. If any of the three actions fail, the others are not rolled back; instead, the failure is logged to the error table and an alert is sent to the scheduler so they can re-send manually if needed. This agent has no external trigger of its own; it is invoked directly by the preceding agent's output. Estimated build time: 10 hours. Complexity: Simple.
// Input (from Availability and Assignment Agent output)
assignment_id : string // used for log correlation
assigned_crew : array // [{ name, email, slack_user_id }]
confirmed_start : datetime // job start for Slack and email copy
confirmed_end : datetime // job end for Slack and email copy
site_address : string // included in all three notifications
customer_name : string // included in Slack and Gmail
customer_email : string // Gmail recipient address
customer_phone : string // included in Gmail confirmation
scope_notes : string // included in Slack message for crew
calendar_event_link : string // included in Slack message for crew
jobber_job_id : string // used for HubSpot deal lookup
exception_flag : boolean // agent must halt if true
// Output (logged on completion)
slack_message_ts : string // Slack message timestamp for audit
gmail_message_id : string // Gmail sent message ID
hubspot_deal_id : string // HubSpot deal ID updated
hubspot_property : string // property name written: 'scheduled_job_date'
sync_status : object // { slack: 'sent'|'failed', gmail: 'sent'|'failed', hubspot: 'updated'|'failed' }
completed_at : datetime // ISO 8601 timestamp of agent completion- Slack API: use the Slack Web API chat.postMessage method. The bot token must have the chat:write and users:read OAuth scopes. crew_slack_user_id values must be stored in the crew calendar config variable alongside calendar IDs. If a crew member has no Slack user ID on record, fall back to posting in the designated crew channel (channel ID stored in credential store) and tagging by name.
- Gmail API: use Gmail API v1 via OAuth 2.0 (not SMTP). The OAuth client must have the gmail.send scope only. The sending address must be a verified Gmail or Google Workspace address owned by [YourCompany.com]. Confirm the sending address before build. The email template must be stored as an editable HTML template in the automation platform's template store.
- Gmail template fields: the confirmation email must interpolate customer_name, confirmed_start (formatted as human-readable date and time), confirmed_end, site_address, and customer_phone. The crew lead name (first entry in assigned_crew array) must also appear in the email body.
- HubSpot CRM API: use HubSpot CRM API v3. Look up the deal by jobber_job_id stored in a custom HubSpot deal property named 'jobber_job_id' (must be created in HubSpot before build). Update the deal property 'scheduled_job_date' (date type) with confirmed_start. Confirm the HubSpot subscription tier includes custom properties and API access (Professional plan or above required).
- Parallel execution: Slack, Gmail, and HubSpot actions may run in parallel branches rather than sequentially to reduce total agent runtime. Each branch must have independent error handling so a Gmail failure does not prevent the Slack message from sending.
- Error logging: on any action failure, write a row to the Supabase error log table (see section 04) with assignment_id, failed_action, error_code, error_message, and timestamp. Send a Slack DM alert to the scheduler user ID immediately after logging.
- Dedupe: check assignment_id against the Supabase log before executing. If this assignment_id has already been processed successfully (all three sync_status values 'sent' or 'updated'), skip re-execution to prevent duplicate notifications on webhook retries.
03End-to-end data flow
// ─────────────────────────────────────────────────────────────────
// TRIGGER: Jobber webhook fires on job.status_changed = 'confirmed'
// ─────────────────────────────────────────────────────────────────
INBOUND webhook_payload {
job_id : 'JB-00421'
job_status : 'confirmed'
job_title : 'Roof Inspection - 14 Maple St'
required_date : '2025-07-08'
required_window : { start: '2025-07-08T08:00:00Z', end: '2025-07-08T12:00:00Z' }
site_address : '14 Maple Street, Springfield, ST 00100'
customer_name : 'Jordan Riley'
customer_phone : '+1 555 201 4400'
scope_notes : 'Full ridge and valley inspection, 2-storey access required'
crew_required : 2
}
// ─────────────────────────────────────────────────────────────────
// VALIDATION: HMAC-SHA256 signature check on X-Jobber-Hmac-SHA256 header
// ─────────────────────────────────────────────────────────────────
validate_signature(webhook_payload, secret) -> pass | reject_with_401
// ─────────────────────────────────────────────────────────────────
// DEDUPE CHECK: query Supabase table 'scheduling_runs'
// ────────────────────────────────────────��────────────────────────
SELECT assignment_id FROM scheduling_runs WHERE job_id = 'JB-00421'
-> no row found: proceed
-> row found with status = 'complete': halt (duplicate webhook)
// ─────────────────────────────────────────────────────────────────
// AGENT 1: Availability and Assignment Agent
// ─────────────────────────────────────────────────────────────────
// Step 1: Query Google Calendar freebusy for all crew
Google_Calendar.freebusy.query({
timeMin : '2025-07-08T08:00:00Z'
timeMax : '2025-07-08T12:00:00Z'
items : [ { id: 'crew1@yc.com' }, { id: 'crew2@yc.com' }, { id: 'crew3@yc.com' } ]
}) -> busy_map { crew1: [], crew2: [{ start, end }], crew3: [] }
// Step 2: Filter available crew
available_crew = [ crew1@yc.com, crew3@yc.com ] // crew2 has conflict
// Step 3: Apply zone / proximity sort (if zone field populated on job)
ranked_crew = sort_by_zone(available_crew, job_zone='North')
-> assigned_crew = [ { id: 'CREW-01', name: 'Alex Dolan', email: 'crew1@yc.com', slack_user_id: 'U04XXALEX' },
{ id: 'CREW-03', name: 'Sam Park', email: 'crew3@yc.com', slack_user_id: 'U04XXSAM' } ]
// Step 4: Create Google Calendar event
Google_Calendar.events.insert({
calendarId : 'primary'
summary : 'Roof Inspection - 14 Maple St [JB-00421]'
start : { dateTime: '2025-07-08T08:00:00Z' }
end : { dateTime: '2025-07-08T12:00:00Z' }
location : '14 Maple Street, Springfield, ST 00100'
description : 'Job ID: JB-00421 | Customer: Jordan Riley | Ph: +1 555 201 4400 | Notes: Full ridge and valley inspection, 2-storey access required'
attendees : [ { email: 'crew1@yc.com' }, { email: 'crew3@yc.com' } ]
status : 'confirmed'
visibility : 'private'
}) -> calendar_event_id: 'GCAL-EVENT-88821'
-> calendar_event_link: 'https://calendar.google.com/event?eid=...'
// Step 5: Write assignment back to Jobber
Jobber.PATCH /v2/jobs/JB-00421 {
assigned_to : [ 'CREW-01', 'CREW-03' ]
scheduled_start : '2025-07-08T08:00:00Z'
scheduled_end : '2025-07-08T12:00:00Z'
} -> jobber_write_status: 200 OK
// Step 6: Generate assignment_id and insert run record
assignment_id = uuid() // e.g. 'a3f9-11ee-...'
Supabase.INSERT scheduling_runs { job_id, assignment_id, status: 'assigned', created_at }
// ─────────────────────────────────────────────────────────────────
// AGENT HANDOFF: Availability and Assignment Agent -> Notifications and CRM Sync Agent
// Payload: assignment_id, assigned_crew[], confirmed_start, confirmed_end,
// site_address, customer_name, customer_email, customer_phone,
// scope_notes, calendar_event_link, jobber_job_id, exception_flag=false
// ─────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────
// AGENT 2: Notifications and CRM Sync Agent (3 parallel branches)
// ─────────────────────────────────────────────────────────────────
// Branch A: Slack crew notification
Slack.chat.postMessage({
channel : 'U04XXALEX' // DM to Alex Dolan
text : ':wrench: New job assigned: Roof Inspection - 14 Maple St | Tue 8 Jul, 8:00am-12:00pm | 14 Maple Street, Springfield | Customer: Jordan Riley | Notes: Full ridge and valley inspection, 2-storey access required | Calendar: https://calendar.google.com/event?eid=...'
}) -> slack_message_ts: '1720387200.000100'
Slack.chat.postMessage({ channel: 'U04XXSAM', text: '<same template>' })
// Branch B: Gmail customer confirmation
Gmail.users.messages.send({
to : 'jordan.riley@example.com'
subject : 'Your booking is confirmed - 8 July 2025'
body : 'Hi Jordan, your booking is confirmed for Tuesday 8 July, 8:00am to 12:00pm at 14 Maple Street, Springfield. Your crew lead is Alex Dolan. On-the-day contact: +1 555 201 4400. Please reply to this email with any questions.'
}) -> gmail_message_id: 'msg_18f3a...'
// Branch C: HubSpot deal update
HubSpot.CRM.deals.search({ filter: { property: 'jobber_job_id', value: 'JB-00421' } })
-> hubspot_deal_id: '88214399'
HubSpot.CRM.deals.update('88214399', {
properties: { scheduled_job_date: '2025-07-08' }
}) -> hubspot_write_status: 200 OK
// ─────────────────────────────────────────────────────────────────
// COMPLETION: Update Supabase run record
// ─────────────────────────────────────────────────────────────────
Supabase.UPDATE scheduling_runs SET {
status : 'complete'
slack_message_ts : '1720387200.000100'
gmail_message_id : 'msg_18f3a...'
hubspot_deal_id : '88214399'
completed_at : '2025-07-07T14:31:05Z'
}
// ─────────────────────────────────────────────────────────────────
// EXCEPTION PATH (if no crew available at Step 2 above)
// ─────────────────────────────────────────────────────────────────
exception_flag = true
Slack.chat.postMessage({
channel : 'SCHEDULER_USER_ID' // stored in credential store
text : ':warning: No crew available for JB-00421 on 8 Jul 08:00-12:00. Manual assignment required.'
})
Supabase.INSERT scheduling_runs { job_id, assignment_id, status: 'exception', exception_reason, created_at }
// Agent halts. No calendar event created.04Recommended build stack
More documents for this process
Every document generated for Job / Project Scheduling.