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
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
02Agent specifications
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.
// 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.
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.
// 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.
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.
// 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.
03End-to-end data flow
// ============================================================
// 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 Close04Recommended build stack
More documents for this process
Every document generated for Sales Pipeline Management.