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 Rep Activity Tracking
[YourCompany.com] · Sales Department · Prepared by FullSpec · [Today's Date]
This document gives the FullSpec build team everything needed to implement the Sales Rep Activity Tracking automation from first credential to live deployment. It covers the current-state process map with bottleneck flags, full agent specifications with IO contracts, the end-to-end data flow trace, and the recommended build stack. The FullSpec team owns all build, configuration, and testing work described here. Your team's responsibilities are limited to providing tool credentials, confirming HubSpot property names, and approving the pilot output before full go-live.
01Process snapshot
02Agent specifications
Monitors Gmail, Google Calendar, and Zoom for completed sales interactions. On each trigger event, the agent extracts structured information from the event metadata or Zoom transcript, matches the attendee or recipient to an existing HubSpot contact and deal, and writes a clean, standardised activity note to HubSpot without any rep input. This agent is the entry point for the entire workflow and must handle three distinct trigger paths reliably: calendar event completion, Zoom meeting end, and outbound Gmail send. The agent must also handle the no-match case gracefully, routing unmatched records to a triage list rather than silently failing.
// Input
calendar_event: { event_id, title, start_time, end_time, attendees[email], description }
zoom_meeting: { meeting_id, host_email, participant_emails[], transcript_text, duration_seconds }
gmail_message: { message_id, from_email, to_emails[], subject, body_text, sent_at }
// Contact match lookup (HubSpot)
hubspot_contact: { contact_id, associated_deal_ids[], owner_rep_email }
// Output (on successful match)
hubspot_activity: {
contact_id,
deal_id,
activity_type, // 'Call' | 'Meeting' | 'Email'
outcome, // 'Connected' | 'Left Voicemail' | 'No Answer' | 'Sent'
note_text, // AI-structured summary: topics, outcomes, next_steps
timestamp, // ISO 8601 UTC
source_signal // 'calendar' | 'zoom' | 'gmail'
}
// On no-match
triage_record: { signal_type, attendee_email, timestamp, reason: 'contact_not_found' }- HubSpot tier must be Starter or above to access the Engagements API (v3) for writing activity logs. Confirm the account tier before build begins.
- Gmail trigger requires the Gmail API with the 'gmail.readonly' OAuth scope and a push notification subscription via Pub/Sub or polling at no more than 60-second intervals to stay within rate limits.
- Google Calendar trigger uses the Calendar API 'calendar.events.readonly' scope. Watch subscriptions expire after 7 days and must be auto-renewed by the orchestration layer.
- Zoom trigger relies on the Zoom Webhook (Event Subscription) for the 'meeting.ended' event and a separate call to the Zoom Recordings API to retrieve the transcript. Zoom cloud recording with auto-transcription must be enabled on the account. If not enabled, the agent falls back to calendar metadata only and sets a flag 'transcript_available: false' on the HubSpot note.
- Contact matching uses the primary email address only. Where multiple attendees are present, the agent matches against all non-host emails in sequence and associates the activity with the first matched HubSpot contact. Where no match is found, the record is written to the triage table rather than dropped.
- Activity type mapping: calendar event maps to 'Meeting', Zoom meeting maps to 'Call', Gmail send maps to 'Email'. Outcome defaults to 'Connected' for completed meetings, 'Sent' for email, and must be overridable per deal stage if required.
- Dedupe: before writing, the agent checks HubSpot for an existing engagement within a 5-minute window on the same contact and source to prevent double-logging if both calendar and Zoom signals fire for the same event.
- Confirm HubSpot custom property names for 'activity_outcome' and any custom activity types before build. Do not assume default HubSpot field names match the client's configured schema.
Reads the activity note produced by the Activity Capture Agent immediately after it is written to HubSpot. The agent scans the note text for explicit next-step phrases and date references, extracts or infers the due date, and creates a HubSpot task assigned to the owning rep against the matched deal. Where no specific date is found in the note, the task due date defaults to three business days from the activity timestamp. This agent runs synchronously after the Activity Capture Agent completes and does not require a separate scheduled trigger.
// Input
engagement_id: string // HubSpot engagement ID from Activity Capture Agent
note_text: string // Full structured note text
contact_id: string
deal_id: string
rep_owner_email: string
activity_timestamp: string // ISO 8601 UTC
// NLP extraction
next_step_phrase: string | null // e.g. 'Send proposal by Friday', 'Follow up next Tuesday'
extracted_date: string | null // ISO 8601 date parsed from phrase
fallback_date: string // activity_timestamp + 3 business days
// Output
hubspot_task: {
task_type: 'TODO',
subject: string, // derived from next_step_phrase or 'Follow up: [contact name]'
due_date: string, // extracted_date ?? fallback_date
body: string, // first 280 chars of note_text
owner_id: string, // HubSpot owner ID mapped from rep_owner_email
associations: { contact_id, deal_id }
}- Date extraction must handle natural-language formats including 'next Tuesday', 'end of week', 'in two weeks', and explicit dates such as '12 June'. Use a deterministic date parsing library rather than a general LLM call for this step to ensure predictability.
- If note_text contains no detectable next-step phrase, the agent still creates a task with the fallback due date and subject 'Follow up: [contact first name]'. It must not silently skip task creation.
- HubSpot task owner_id requires a lookup from rep_owner_email to HubSpot internal owner ID. Maintain a small static map of rep emails to owner IDs and flag any missing mapping to the error log rather than assigning to a default owner.
- Google Calendar integration is optional at this stage: if the client wants tasks mirrored as calendar events on the rep's calendar, this can be added as a secondary action using the Calendar API 'calendar.events' write scope. Confirm scope of this feature before build.
- Confirm whether HubSpot tasks should also trigger a HubSpot notification to the rep or whether Slack is the preferred notification channel. Avoid double-notifying.
Runs on a scheduled daily trigger at 6:00 PM local time. The agent queries HubSpot for all activity logged during the current day across all rep owner IDs, appends a summary row per rep to the Google Sheets activity tracker, and then constructs and posts a formatted Slack digest to the designated sales channel. The digest includes per-rep activity counts for the day and flags any HubSpot contacts whose last-activity date is seven or more days in the past, marking them as stale. The stale-contact threshold is configurable via an environment variable.
// Input
hubspot_query: {
filter: { last_modified_date: today, object_type: 'engagements' },
group_by: owner_id
}
stale_threshold_days: integer // env var, default 7
rep_roster: [{ owner_id, rep_name, rep_email }] // static config or HubSpot owners API
// Intermediate
daily_summary: [
{ rep_name, calls_logged, emails_logged, meetings_logged, tasks_created, total_activities }
]
stale_contacts: [{ contact_id, contact_name, last_activity_date, owner_rep_name }]
// Output: Google Sheets row (appended)
sheets_row: [date, rep_name, calls, emails, meetings, tasks, total, stale_flag_count]
// Output: Slack message (sales channel)
slack_block: {
header: 'Daily Activity Digest — [date]',
sections: [ per_rep_activity_counts ],
stale_section: 'Contacts with no activity in 7+ days: [list]',
footer: 'Powered by FullSpec | support@gofullspec.com'
}- Google Sheets append requires the Sheets API with 'spreadsheets.values.append' scope. The target spreadsheet ID and sheet tab name must be confirmed and stored in the credential store before build.
- Slack posting uses an incoming webhook URL or a Bot Token with 'chat:write' scope targeting a specific channel ID. Confirm the channel name and whether the Bot Token or webhook approach is preferred for the client's Slack workspace tier.
- The stale-contact query must exclude contacts in deal stages marked as 'Closed Won' or 'Closed Lost' to avoid flagging completed deals. Confirm the exact HubSpot deal stage names and IDs before implementing the filter.
- If zero activities are logged for a rep on a given day (for example, during a leave day), the agent still appends a row with zero counts rather than skipping the rep. This ensures the Sheets tracker has a complete daily record.
- The Slack digest is sent at 6:00 PM rather than 8:00 AM as originally scoped in the automated canvas. Confirm preferred send time with the sales manager before finalising the trigger schedule.
- HubSpot Engagements API (v3) rate limit is 150 requests per 10 seconds. The daily batch query should use the search endpoint with pagination rather than per-contact polling to stay within limits.
03End-to-end data flow
// ─────────────────────────────────────────────────────────────────────
// TRIGGER LAYER — three parallel entry points
// ─────────────────────────────────────────────────────────────────────
TRIGGER_A: Google Calendar API (push notification)
event.status == 'confirmed' AND event.end_time <= NOW()
fields: event_id, title, start_time, end_time, attendees[email], description
TRIGGER_B: Zoom Webhook 'meeting.ended'
fields: meeting_id, host_email, participant_emails[], duration_seconds
→ follow-up call: Zoom Recordings API GET /v2/meetings/{meetingId}/recordings
fields appended: transcript_vtt_url, recording_files[file_type='TRANSCRIPT']
TRIGGER_C: Gmail API (Pub/Sub push or polling, max 60s interval)
filter: from == rep_email AND to IN hubspot_contact_emails
fields: message_id, from_email, to_emails[], subject, body_text, sent_at
// ─────────────────────────────────────────────────────────────────────
// AGENT 1: Activity Capture Agent
// ─────────────────────────────────────────────────────────────────────
STEP 1.1 — Contact match
input: attendee_email | gmail_recipient | zoom_participant_email
action: HubSpot Contacts API GET /crm/v3/objects/contacts?email={email}
output: contact_id (string) | null
on null: write to triage_table { signal_type, email, timestamp, reason:'contact_not_found' }
→ HALT this branch; no further processing
STEP 1.2 — Deal association lookup
input: contact_id
action: HubSpot Associations API GET /crm/v3/objects/contacts/{id}/associations/deals
output: deal_ids[] (use first open deal by create_date DESC)
rep_owner_id (from deal.properties.hubspot_owner_id)
STEP 1.3 — Dedupe check
action: HubSpot Engagements search for existing engagement on contact_id
within window: [NOW() - 5min, NOW() + 5min], type matching signal source
on duplicate found: → HALT; log 'duplicate_skipped' to error_log
STEP 1.4 — Note generation
input (calendar path): event.title, event.description, event.duration, attendees[]
input (zoom path): transcript_text (parsed from VTT), meeting.duration_seconds
input (gmail path): email.subject, email.body_text (truncated to 4000 chars)
output: structured_note {
activity_type: 'Meeting' | 'Call' | 'Email'
outcome: 'Connected' | 'Left Voicemail' | 'Sent'
summary: string // 2-4 sentences: topics covered, decisions made
next_steps: string // extracted commitments and follow-up intent
transcript_available: boolean
}
STEP 1.5 — Write HubSpot engagement
action: HubSpot Engagements API POST /crm/v3/objects/engagements
payload: { type, outcome, timestamp, note_text, associations:{ contact_id, deal_id } }
output: engagement_id (string) → passed to Agent 2
// ─────────────────────────────────────────────────────────────────────
// AGENT 2: Follow-Up and Task Agent
// ─────────────────────────────────────────────────────────────────────
STEP 2.1 — Next-step extraction
input: structured_note.next_steps (string)
action: date parser (deterministic NLP library, not LLM)
output: extracted_date (ISO 8601) | null
fallback_date = activity_timestamp + 3 business days
task_subject = next_steps phrase | 'Follow up: {contact.firstname}'
STEP 2.2 — Owner ID resolution
input: rep_owner_email
action: static map lookup { email -> hubspot_owner_id }
on miss: log 'owner_not_found' to error_log; skip task creation
STEP 2.3 — Create HubSpot task
action: HubSpot Tasks API POST /crm/v3/objects/tasks
payload: {
hs_task_type: 'TODO',
hs_task_subject: task_subject,
hs_timestamp: due_date (ISO 8601),
hs_task_body: note_text[0:280],
hubspot_owner_id: owner_id,
associations: { contact_id, deal_id }
}
output: task_id (string)
// ─────────────────────────────────────────────────────────────────────
// AGENT 3: Reporting and Nudge Agent (scheduled 18:00 daily)
// ───────────────────────────────────────────────────────────────���─────
STEP 3.1 — HubSpot daily activity query
action: HubSpot Engagements Search API POST /crm/v3/objects/engagements/search
filter: { last_modified_date: { gte: TODAY_00:00, lte: TODAY_23:59 } }
group results by: hubspot_owner_id
output: daily_summary[] { owner_id, calls, emails, meetings, tasks }
STEP 3.2 — Stale contact query
action: HubSpot Contacts Search, filter { notes_last_activity_date: { lte: NOW()-7d } }
exclude deal_stage IN ['closedwon','closedlost']
output: stale_contacts[] { contact_id, firstname, lastname, owner_id, last_activity_date }
STEP 3.3 — Append Google Sheets row
action: Sheets API POST /v4/spreadsheets/{spreadsheetId}/values/{range}:append
row: [date, rep_name, calls, emails, meetings, tasks, total, stale_count]
output: updated_range (confirmation string)
STEP 3.4 — Post Slack digest
action: Slack API POST /chat.postMessage OR incoming webhook URL
channel: sales_channel_id (env var)
payload: Block Kit message {
header block: 'Daily Activity Digest — {date}'
section blocks: one per rep { rep_name, calls, emails, meetings, tasks, total }
stale block: 'Contacts needing attention (7+ days): {stale_contact_list}'
context block: 'Powered by FullSpec | support@gofullspec.com'
}
output: slack_message_ts (timestamp, stored for audit log)04Recommended build stack
More documents for this process
Every document generated for Sales Rep Activity Tracking.