Back to Sales Pipeline Management

Developer Handover Pack

Everything needed to build the automation from scratch: current state, full agent specs, data flow, and the build stack.

4 pagesPDF · Technical
FS-DOC-04Technical

Developer Handover Pack

Sales Pipeline Management

[YourCompany.com] · Sales Department · Prepared by FullSpec · [Today's Date]

This document gives the FullSpec build team everything needed to implement the three-agent Sales Pipeline Management automation from first credential to production launch. It covers the current-state process map with bottlenecks flagged, full agent specifications with IO contracts, the end-to-end data flow, and the recommended build stack with estimated hours. The FullSpec team owns all build and integration work. Your team's responsibilities are limited to supplying API credentials, confirming HubSpot field names and territory rules, and approving templates before go-live.

01Process snapshot

Step
Name
Description
1
Log New Deal in CRM
Sales Rep manually creates a deal record in HubSpot, entering contact details, deal value, and expected close date. Incomplete records are common during high-call periods. Time cost: 10 min/deal.
2
Assign Deal Owner and Stage
Rep or Sales Manager manually assigns the deal to the correct owner and selects the starting pipeline stage. Misassignments occur when territory rules are undocumented. Time cost: 5 min/deal.
3
Schedule Initial Follow-Up Task
Rep manually creates a follow-up task or meeting reminder in HubSpot or calendar. Frequently skipped during busy periods, leaving deals with no next action. Time cost: 7 min/deal.
4
Send Introduction or Proposal Email
Rep drafts and sends a personalised intro or proposal email from Gmail using manually copied templates. Requires editing for each prospect. Time cost: 20 min/deal.
5
Log Call and Meeting Notes
Rep manually types interaction notes into the HubSpot deal record. Logging is often delayed by hours or completed in bulk at end of day, reducing accuracy. Time cost: 10 min/interaction.
6
Update Deal Stage Manually
Rep must remember to move the deal to the next pipeline stage after each meaningful interaction. Consistently the most skipped step across sales teams. Time cost: 5 min/interaction.
7
Check for Stalled Deals
Sales Manager manually reviews the pipeline view in HubSpot for deals with no recent activity. Happens weekly at best and misses mid-week stalls entirely. Time cost: 30 min/week.
8
Send Stall Alert to Rep
When a stalled deal is spotted, the manager sends a Slack message or email to the rep to re-engage. Depends on the manager having time to complete step 7. Time cost: 10 min/week.
9
Prepare Weekly Pipeline Report
Manager exports pipeline data from HubSpot to Google Sheets, formats it, and shares with leadership. Data is often a day old by the time it is shared. Time cost: 45 min/week.
10
Mark Deal as Won or Lost
Rep manually closes the deal in HubSpot, selecting outcome and entering a close reason. Lost reasons are frequently left blank, preventing pattern analysis. Time cost: 8 min/deal.
Time cost summary: Total manual time per cycle (single deal, end-to-end) is 150 minutes (2.5 hours). Across approximately 15 to 25 stage changes per week and recurring manager tasks, the process consumes 7 hours of staff time per week (350 hours per year at a blended rate of $50/hour, totalling $18,200/year). The automation replaces steps 1, 2, 3, 4, 6 (Pipeline Admin Agent), steps 7 and 8 (Deal Health Monitor), and steps 9 and 10 (Pipeline Reporting Agent). Step 5, logging call and meeting notes, remains a human task requiring rep judgement.
Developer Handover PackPage 1 of 4
FS-DOC-04Technical

02Agent specifications

Pipeline Admin Agent

Handles all routine CRM housekeeping that reps consistently skip: validating deal records on creation, auto-assigning owners using territory and value-tier rules, updating pipeline stages, creating follow-up tasks with stage-appropriate due dates, and sending stage-triggered emails from the assigned rep's Gmail account. This agent fires on every deal creation event and every stage-change event in HubSpot, including deals booked through Calendly. It is the highest-complexity agent in the build because it touches four integrated tools and must apply conditional routing logic for owner assignment and email template selection.

Trigger
HubSpot webhook: deal.creation and deal.propertyChange (pipeline stage field). Also fires on Calendly booking webhook when mapped to a HubSpot deal.
Tools
HubSpot (CRM read/write), Gmail (send-on-behalf via OAuth), Calendly (inbound webhook)
Replaces steps
1, 2, 3, 4, 6
Estimated build
20 hours | Complex
// Input
deal.id               (string)   HubSpot deal record ID
deal.stage            (string)   Current pipeline stage key
deal.owner_id         (string)   Assigned owner HubSpot user ID (may be null on creation)
deal.amount           (number)   Deal value in USD
deal.closedate        (ISO8601)  Expected close date
contact.email         (string)   Primary contact email address
contact.firstname     (string)   Contact first name for email merge
contact.company       (string)   Company name for email merge
calendly.event_id     (string)   Present only when triggered via Calendly booking

// Processing
IF deal.owner_id IS NULL -> apply territory_rules[] to assign owner
IF deal.stage IS NULL OR invalid -> set to default stage: 'appointment_scheduled'
SELECT email_template WHERE template.stage_key = deal.stage
CREATE task: { type: 'follow_up', due_date: NOW + stage_sla_days[deal.stage], owner: deal.owner_id }

// Output
HubSpot deal.owner_id       updated to assigned rep user ID
HubSpot deal.stage          confirmed or corrected pipeline stage key
HubSpot task.id             new follow-up task linked to deal
Gmail message.id            sent email ID logged against deal timeline
HubSpot engagement.id       email engagement record written to deal
  • HubSpot tier requirement: Sales Hub Starter or above is required for deal-based workflow triggers and task automation via API. Confirm the active subscription tier before build starts.
  • Gmail send-on-behalf: Each rep's Gmail account must be connected individually via OAuth 2.0 with the gmail.send scope. A Google Workspace admin may need to approve the OAuth client. Do not use a shared mailbox for sending.
  • Territory rules table: Owner assignment logic must be provided in a structured table (rep name, HubSpot user ID, deal value range, region/territory). The automation cannot be built until this table is finalised and confirmed by the sales manager.
  • Email templates: One approved HTML template per active pipeline stage must be provided before build. Templates must include merge tokens {{contact.firstname}}, {{deal.name}}, and {{rep.signature}} at minimum.
  • Calendly mapping: Confirm the Calendly event type name(s) that should trigger deal creation. The webhook payload's invitee.email field is used to look up or create the HubSpot contact.
  • Dedupe behaviour: Before creating a new deal, the agent queries HubSpot contacts API by email. If a matching contact with an open deal in the same stage exists, the agent skips creation and logs a duplicate flag to the error table rather than creating a duplicate record.
  • Fallback on missing owner: If no territory rule matches, the deal is assigned to a designated fallback owner (HubSpot user ID to be confirmed) and a Slack notification is sent to the sales manager.
  • Stage SLA days must be confirmed per pipeline stage before build: these drive the follow-up task due date calculation.
Deal Health Monitor

Watches every open deal for inactivity, running on a daily schedule and also firing immediately when a deal crosses the 5-business-day inactivity threshold. When a stall condition is detected, it sends a targeted Slack notification to the deal owner and their manager, including the deal name, number of days stalled, and a direct HubSpot deal URL. This agent is moderate in complexity: the primary logic is a date-diff calculation against the last logged activity timestamp, with a Slack message dispatch as the output.

Trigger
Scheduled: daily at 08:00 in the account's local timezone. Also triggered immediately when deal.last_activity_date delta exceeds 5 business days (computed in workflow, not via HubSpot workflow).
Tools
HubSpot (deals list with last_activity_date filter), Slack (incoming webhook or bot token with chat:write scope)
Replaces steps
7, 8
Estimated build
10 hours | Moderate
// Input
HubSpot API response: deals[]  filtered to pipeline_stage != 'closedwon' AND != 'closedlost'
deal.id                (string)   HubSpot deal record ID
deal.dealname          (string)   Deal display name for Slack message
deal.last_activity_date (ISO8601) Timestamp of last logged activity
deal.owner_id          (string)   HubSpot user ID of assigned rep
deal.hubspot_owner.manager_id (string) Manager's HubSpot user ID (custom property, to be confirmed)

// Processing
FOR each deal IN deals[]:
  business_days_inactive = calc_business_days(deal.last_activity_date, NOW)
  IF business_days_inactive >= 5 AND deal.stall_alert_sent != TODAY:
    resolve rep.slack_user_id FROM deal.owner_id (lookup table)
    resolve manager.slack_user_id FROM deal.owner.manager_id (lookup table)
    compose Slack message with deal.dealname, business_days_inactive, deal_url
    SET deal.stall_alert_sent = TODAY (custom HubSpot property) to prevent duplicate alerts

// Output
Slack message to rep.slack_user_id    direct message with deal context and CRM link
Slack message to manager.slack_user_id copy of same alert for manager visibility
HubSpot deal.stall_alert_sent         updated to today's date to prevent re-alert within same day
  • Business-day calculation must exclude weekends and any public holidays relevant to the account's locale. Confirm the locale and any custom non-working days before build.
  • A custom HubSpot deal property named stall_alert_sent (date type) must be created before build. This prevents the agent from sending duplicate alerts if it runs multiple times in one day.
  • Slack user ID mapping: A lookup table mapping HubSpot owner user IDs to Slack member IDs must be provided and maintained. If a HubSpot owner has no matching Slack ID, the alert falls back to posting in a designated sales-alerts channel.
  • Manager relationship: HubSpot does not natively store a rep-to-manager mapping on deal records. A custom deal property or a static lookup table must be confirmed before build. If the manager field is missing, the Slack alert is sent to the rep only and a flag is written to the error log.
  • Slack bot scope requirement: The Slack app must have chat:write and users:read scopes. If the workspace restricts app installs, a Workspace Admin approval will be required.
  • Confirm the Slack channel or DM preference for alerts: direct messages to both rep and manager is the default behaviour, but a shared channel per territory is an alternative worth confirming with the sales manager.
Pipeline Reporting Agent

Pulls deal data from HubSpot on a daily schedule and writes a structured summary row to a Google Sheets report, covering stage distribution, deal value, days in stage, and last activity. It also fires immediately when any deal is marked won or lost, logging the close reason and outcome to a separate close-reason tab. This is the simplest agent in the build, relying on a well-documented HubSpot deals API response and the Google Sheets API for writes.

Trigger
Scheduled: daily at 08:30 local time (30 min after Deal Health Monitor). Event-based: fires immediately on HubSpot deal.propertyChange where property = dealstage AND new value = 'closedwon' OR 'closedlost'.
Tools
HubSpot (deals search API), Google Sheets (append row via Sheets API v4)
Replaces steps
9, 10
Estimated build
8 hours | Simple
// Input (daily scheduled run)
HubSpot deals search response: deals[] with properties:
  deal.id, deal.dealname, deal.amount, deal.pipeline,
  deal.dealstage, deal.closedate, deal.last_activity_date,
  deal.days_to_close (computed), deal.owner_id

// Input (close event trigger)
deal.id               (string)   Deal record ID
deal.dealstage        (string)   'closedwon' or 'closedlost'
deal.closed_lost_reason (string) Close reason property (may be null if rep left blank)
deal.amount           (number)   Final deal value
deal.closedate        (ISO8601)  Actual close date

// Processing (daily run)
FOR each deal IN deals[]:
  days_in_stage = calc_days(deal.entered_current_stage, NOW)
  MAP deal properties to Sheets row schema (see column map below)
  APPEND row to tab 'Pipeline_Daily' in target Google Sheet
  OVERWRITE existing row if deal.id already present for today's date

// Processing (close event)
IF deal.closed_lost_reason IS NULL -> set to 'Not provided'
APPEND row to tab 'Close_Reasons' with outcome, amount, reason, close date

// Output
Google Sheets tab 'Pipeline_Daily': one row per deal per day
  Columns: Date | Deal ID | Deal Name | Stage | Owner | Amount | Days in Stage | Last Activity | Close Date
Google Sheets tab 'Close_Reasons': one row per closed deal
  Columns: Date | Deal ID | Deal Name | Outcome | Close Reason | Amount | Days to Close
  • Google Sheets file must be created and shared with the service account or OAuth user used by the automation platform before build. The sheet ID and tab names must be confirmed and should not change after go-live.
  • HubSpot deals search API returns a maximum of 100 records per page. The agent must implement pagination using the paging.next.after cursor for accounts with more than 100 open deals.
  • The close reason property in HubSpot (hs_closed_lost_reason or a custom property) must be confirmed. If the team uses a custom dropdown, the internal property name must be provided before build.
  • Duplicate row handling: on the daily run, the agent checks for an existing row with matching deal.id and today's date before appending, overwriting if found. This prevents inflated row counts from multiple daily runs.
  • The Google Sheets OAuth scope required is https://www.googleapis.com/auth/spreadsheets. Confirm that the authenticating Google account has editor access to the target sheet.
  • If a close event fires and deal.closed_lost_reason is null, the agent writes 'Not provided' to the Close_Reasons tab and flags the deal ID in the error log for the sales manager to review.
Developer Handover PackPage 2 of 4
FS-DOC-04Technical

03End-to-end data flow

Full trigger-to-output data flow: Sales Pipeline Management automation
// ============================================================
// TRIGGER EVENTS
// ============================================================

// Event A: Deal created in HubSpot (manual entry or form)
SOURCE: HubSpot webhook -> subscriptionType: 'deal.creation'
PAYLOAD: { objectId: deal.id, portalId: account.id, changeSource: 'CRM' | 'FORM' }

// Event B: Deal stage changed in HubSpot
SOURCE: HubSpot webhook -> subscriptionType: 'deal.propertyChange'
PAYLOAD: { objectId: deal.id, propertyName: 'dealstage', propertyValue: '<stage_key>' }

// Event C: Meeting booked via Calendly
SOURCE: Calendly webhook -> event: 'invitee.created'
PAYLOAD: { event.uuid, invitee.email, invitee.name, event.start_time, event.name }

// ============================================================
// AGENT 1: PIPELINE ADMIN AGENT
// ============================================================

// Step 1.1 - Enrich and validate deal record
GET HubSpot /crm/v3/objects/deals/{deal.id}?properties=dealname,amount,closedate,dealstage,hubspot_owner_id,hs_contact_ids
IF amount IS NULL OR closedate IS NULL -> flag incomplete: true, write to error_log table
GET HubSpot /crm/v3/objects/contacts/{contact.id}?properties=email,firstname,lastname,company
FIELDS RESOLVED: contact.email, contact.firstname, contact.company

// Step 1.2 - Apply territory rules and assign owner
LOOKUP territory_rules[] WHERE deal.amount IN value_range AND contact.region = rep.territory
IF match found -> PATCH HubSpot /crm/v3/objects/deals/{deal.id} { hubspot_owner_id: rep.user_id }
IF no match -> PATCH hubspot_owner_id = fallback_owner.user_id, POST Slack alert to manager
FIELDS WRITTEN: deal.owner_id (HubSpot user ID string)

// Step 1.3 - Send stage-triggered email via Gmail
SELECT email_template WHERE template.stage_key = deal.dealstage
MERGE: template.body.replace('{{contact.firstname}}', contact.firstname)
       template.body.replace('{{deal.name}}', deal.dealname)
       template.body.replace('{{rep.signature}}', rep.email_signature)
POST Gmail /gmail/v1/users/{rep.email}/messages/send { raw: base64_encoded_mime_message }
RESPONSE: { id: message.id, threadId: thread.id }
LOG: POST HubSpot /crm/v3/objects/emails (engagement) linked to deal.id and contact.id
FIELDS WRITTEN: gmail.message_id, HubSpot engagement.id

// Step 1.4 - Create follow-up task in HubSpot
LOOKUP stage_sla_days WHERE stage_key = deal.dealstage -> sla_days (integer)
POST HubSpot /crm/v3/objects/tasks {
  hs_task_subject: 'Follow up: ' + deal.dealname,
  hs_timestamp: NOW + sla_days (milliseconds),
  hubspot_owner_id: deal.owner_id,
  hs_task_status: 'NOT_STARTED'
}
ASSOCIATE: POST /crm/v3/objects/tasks/{task.id}/associations/deals/{deal.id}
FIELDS WRITTEN: HubSpot task.id, task.due_date

// ============================================================
// AGENT 2: DEAL HEALTH MONITOR
// Runs daily at 08:00 (and on inactivity threshold breach)
// ============================================================

// Step 2.1 - Fetch all open deals with last activity date
POST HubSpot /crm/v3/objects/deals/search {
  filterGroups: [{ filters: [{ propertyName: 'dealstage', operator: 'NOT_IN', values: ['closedwon','closedlost'] }] }],
  properties: ['dealname','hubspot_owner_id','last_activity_date','stall_alert_sent','hs_object_id'],
  limit: 100, after: cursor (paginate until no next cursor)
}
FIELDS RESOLVED: deal.id, deal.dealname, deal.last_activity_date, deal.stall_alert_sent, deal.owner_id

// Step 2.2 - Calculate inactivity and dispatch alerts
FOR each deal IN deals[]:
  business_days_inactive = calc_business_days(deal.last_activity_date, NOW, exclude_weekends=true)
  IF business_days_inactive >= 5 AND deal.stall_alert_sent != TODAY:
    LOOKUP slack_user_map WHERE hubspot_user_id = deal.owner_id -> rep.slack_user_id
    LOOKUP manager_map WHERE rep.user_id = deal.owner_id -> manager.slack_user_id
    POST Slack API /chat.postMessage {
      channel: rep.slack_user_id,
      text: 'Deal stalled: ' + deal.dealname + ' | ' + business_days_inactive + ' days inactive | ' + deal_url
    }
    POST Slack API /chat.postMessage { channel: manager.slack_user_id, text: same }
    PATCH HubSpot deal.stall_alert_sent = TODAY (ISO8601 date)
FIELDS WRITTEN: HubSpot deal.stall_alert_sent, Slack message.ts (logged to error_log on failure)

// ============================================================
// AGENT 3: PIPELINE REPORTING AGENT
// Daily run 08:30 + immediate on deal closed event
// ============================================================

// Step 3.1 - Daily pipeline snapshot to Google Sheets
POST HubSpot /crm/v3/objects/deals/search { all open deals, paginated }
FOR each deal IN deals[]:
  days_in_stage = calc_days(deal.hs_stage_timestamp, NOW)
  ROW = [TODAY, deal.id, deal.dealname, deal.dealstage, deal.owner_id, deal.amount,
         days_in_stage, deal.last_activity_date, deal.closedate]
  CHECK Sheets tab 'Pipeline_Daily' for existing row WHERE col[Date]=TODAY AND col[Deal ID]=deal.id
  IF exists -> UPDATE row | IF not exists -> APPEND row
POST Sheets API /v4/spreadsheets/{sheet.id}/values/'Pipeline_Daily'!A:I:append { values: [ROW] }
FIELDS WRITTEN: Pipeline_Daily columns: Date, Deal ID, Deal Name, Stage, Owner, Amount, Days in Stage, Last Activity, Close Date

// Step 3.2 - Close event: log outcome to Close_Reasons tab
TRIGGER: HubSpot deal.propertyChange WHERE dealstage IN ['closedwon','closedlost']
GET deal.closed_lost_reason (hs_closed_lost_reason or custom property)
IF closed_lost_reason IS NULL -> value = 'Not provided', flag in error_log
ROW = [TODAY, deal.id, deal.dealname, deal.dealstage, closed_lost_reason, deal.amount, days_to_close]
POST Sheets API /v4/spreadsheets/{sheet.id}/values/'Close_Reasons'!A:G:append { values: [ROW] }
FIELDS WRITTEN: Close_Reasons columns: Date, Deal ID, Deal Name, Outcome, Close Reason, Amount, Days to Close
Developer Handover PackPage 3 of 4
FS-DOC-04Technical

04Recommended build stack

Orchestration layer
A workflow automation tool (no specific platform is mandated at this stage). Build one discrete workflow per agent: 'Pipeline Admin Agent', 'Deal Health Monitor', and 'Pipeline Reporting Agent'. All three workflows share a single credential store so that HubSpot, Gmail, Slack, and Google Sheets connections are defined once and reused. Do not hard-code API keys or tokens inside individual workflow nodes.
Webhook configuration
HubSpot webhooks: configure two subscription types in HubSpot Settings > Integrations > Private Apps: 'deal.creation' and 'deal.propertyChange' (targeting the 'dealstage' property and the 'closedwon'/'closedlost' values). Calendly webhook: configure 'invitee.created' event in the Calendly developer console pointing to the Pipeline Admin Agent webhook URL. All inbound webhook endpoints must be HTTPS and must validate the HubSpot or Calendly signature header before processing.
Templating approach
Email bodies are stored as HTML templates outside the workflow (in a version-controlled file or a dedicated template store), referenced by stage key at runtime. Merge token substitution is performed inside the workflow node before the Gmail API call. Template files must be named by stage key (e.g. 'appointment_scheduled.html', 'proposal_sent.html') and approved by the sales manager before go-live. Slack message text is composed inline using a text-builder node with variables injected from the deal payload.
Error logging
All agent failures, incomplete deal flags, missing owner matches, null close reasons, and Slack delivery failures are written to a Supabase table named 'pipeline_automation_errors' with columns: id (uuid), timestamp (timestamptz), agent_name (text), deal_id (text), error_type (text), error_detail (text), resolved (boolean). A Slack alert fires to the designated ops channel (to be confirmed) whenever a new error row is inserted. The Supabase project and table must be created and the connection string stored in the credential store before build begins.
Testing approach
All three agents are built and tested against the HubSpot sandbox environment first, using cloned deal records with sanitised contact data. Gmail send-on-behalf is tested using a dedicated test rep account before connecting live rep credentials. Slack alerts are tested to a private test channel before switching to live user DMs. Google Sheets writes are validated against a staging copy of the report sheet. Only after end-to-end sandbox validation does FullSpec migrate configuration to the production environment.
Estimated total build time
Pipeline Admin Agent: 20 hours (Complex). Deal Health Monitor: 10 hours (Moderate). Pipeline Reporting Agent: 8 hours (Simple). End-to-end integration testing, error logging setup, and production migration: 10 hours. Total: 48 hours across a 4 to 5 week delivery window.
Before any build work begins, the following must be confirmed by your team: (1) HubSpot pipeline stage names and internal stage keys, (2) the territory and owner assignment rules table with HubSpot user IDs, (3) one approved email template per active pipeline stage, (4) the Google Sheets file ID and tab names, (5) the Slack user ID lookup table mapping HubSpot owner IDs to Slack member IDs, and (6) a confirmed Google Workspace admin contact for Gmail OAuth approval. Missing any of these will block the build at the relevant step. Raise blockers early via support@gofullspec.com.
Developer Handover PackPage 4 of 4

More documents for this process

Every document generated for Sales Pipeline Management.

Launch Plan
Operations · Owner
View
ROI and Business Case
Finance · Owner
View
Process Runbook / SOP
Operations · Owner
View
Integration and API Spec
Technical · Developer
View
Test and QA Plan
Quality · Developer
View