Back to Upsell & Cross-sell Triggers

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

Upsell & Cross-sell Triggers

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

This document is the primary technical reference for the FullSpec team building the Upsell and Cross-sell Triggers automation. It covers the full current-state process map, both agent specifications with IO contracts, the end-to-end data flow, and the recommended build stack. Everything needed to begin construction, set up integrations, and validate agent logic is contained here. Where the owner side defines what should happen, this document defines how it is built.

01Process snapshot

Step
Name
Description
1
Pull Customer Activity Report
Sales rep exports recent order data and usage metrics from billing and product systems into a spreadsheet, usually weekly. Time cost: 30 min/cycle.
2
Cross-reference CRM for Account Status
Rep checks HubSpot to confirm account health, open deals, and recent communication before flagging any customer as a candidate. Time cost: 25 min/cycle.
3 BOTTLENECK
Identify Upsell and Cross-sell Candidates
Rep manually reviews combined data and decides which customers qualify based on spend, usage level, or product fit. Relies heavily on individual judgement; inconsistent across the team. Time cost: 40 min/cycle.
4
Select the Right Offer for Each Customer
Rep decides which product tier, add-on, or complementary product to recommend based on their knowledge of the account. Time cost: 20 min/cycle.
5 BOTTLENECK
Draft Personalised Outreach Email
Rep writes an individualised email for each candidate referencing account details and the specific offer being proposed. Time cost: 35 min/cycle.
6
Log Outreach in CRM
After sending, rep manually records the activity in HubSpot including the offer sent and date. Time cost: 15 min/cycle.
7
Notify Account Owner in Slack
If the account has a dedicated owner who is not the rep running the exercise, that owner is messaged in Slack. Time cost: 5 min/cycle.
8
Monitor for Customer Response
Rep tracks replies manually, checking inbox and CRM periodically and flagging responses that need follow-up. Time cost: 20 min/cycle.
9
Record Outcome in Tracking Sheet
Win, no response, or decline is logged in the shared Google Sheet so management can review conversion rates. Time cost: 10 min/cycle.
Time cost summary: Total manual time per cycle is 200 minutes (3 hours 20 min). At approximately 120 qualifying events per month the process consumes roughly 400 hours of rep time annually, equivalent to 6 hours per week. Steps 3 and 4 (candidate identification and offer selection) and steps 5, 6, and 7 (email drafting, CRM logging, and Slack notification) are fully replaced by the two agents. Steps 1 and 2 are replaced by the automated Segment trigger and HubSpot fetch. Steps 8 and 9 remain partially manual but benefit from auto-logging reducing the recording burden.
Developer Handover PackPage 1 of 4
FS-DOC-04Technical

02Agent specifications

Offer Selection Agent

Evaluates each triggered customer record against the product catalogue and account history to select the best-fit upsell or cross-sell offer. It applies scoring rules based on spend tier, product usage signals, and account segment so offer selection is consistent and data-driven rather than left to individual rep judgement. The agent reads from a structured offer catalogue stored in Google Sheets and cross-references live CRM data fetched from HubSpot and behavioural event properties passed from Segment. Output is a single ranked recommendation passed downstream to the Outreach Drafting Agent.

Trigger
Fires after the customer CRM record is fetched from HubSpot and verified as eligible for outreach (no open deal in negotiation stage, no suppression flag, account status active).
Tools
HubSpot (account data read), Segment (event properties input), Google Sheets (offer catalogue read)
Replaces steps
Step 3: Identify Upsell and Cross-sell Candidates; Step 4: Select the Right Offer for Each Customer
Estimated build
10 hours, Moderate complexity
// Input
segment_event: {
  event_name: string,          // e.g. 'purchase_completed' | 'usage_threshold_crossed' | 'renewal_approaching'
  customer_id: string,         // Segment anonymous or identified user ID
  event_properties: {
    spend_total_usd: number,
    product_ids_active: string[],
    usage_metric_value: number,
    renewal_date_iso: string | null
  },
  timestamp: string            // ISO 8601
}
hubspot_record: {
  contact_id: string,
  company_id: string,
  account_tier: 'standard' | 'high-value',
  open_deals: Deal[],
  last_outreach_date: string | null,
  suppression_flag: boolean
}
offer_catalogue: OfferRow[]    // fetched from Google Sheets at runtime

// Output
offer_selection: {
  selected_offer_id: string,
  selected_offer_name: string,
  offer_rationale: string,     // max 2 sentences for use in email body
  score: number,               // 0-100 composite score
  account_tier: 'standard' | 'high-value',
  customer_id: string,
  contact_id: string,
  company_id: string
}
  • Implementation note: The offer catalogue Google Sheet must follow a fixed schema before build begins. Required columns are offer_id, offer_name, min_spend_usd, eligible_product_ids (pipe-delimited), account_segments (pipe-delimited), and active (boolean). The agent reads this sheet at workflow runtime, not at deploy time, so updates to the catalogue take effect immediately without a redeploy.
  • Implementation note: Scoring weights are spend tier (40%), active product overlap (35%), and usage proximity to next threshold (25%). These weights must be confirmed by the sales team and stored as configurable variables in the orchestration layer, not hardcoded into the agent logic.
  • Implementation note: If the HubSpot record has suppression_flag: true or an open deal at stage 'proposal' or later, the agent must short-circuit and write a suppression reason to the error log table before exiting. No offer is selected and the workflow terminates cleanly.
  • Implementation note: If no offer scores above 40 out of 100, the agent must write a no-match record to the Google Sheets tracking tab with reason 'below_score_threshold' and exit without calling the Outreach Drafting Agent.
  • Implementation note: Dedupe check required: before emitting output, query HubSpot activity log for any outreach of type 'upsell_automation' on the same contact_id within the last 30 days. If found, suppress and log 'duplicate_suppressed'.
  • Requires HubSpot Sales Hub Starter or above for activity logging API access. Confirm tier before build.
  • Requires Segment Source with identify and track calls enabled. Confirm event taxonomy matches trigger condition names exactly before build.
Outreach Drafting Agent

Writes a personalised outreach email for each eligible customer using the offer recommendation from the Offer Selection Agent, CRM account data from HubSpot, and stored email tone guidelines. The draft references the customer by name, cites their specific account context (e.g. spend level, active products, or renewal proximity), and positions the offer in plain language. Once the draft is produced the agent routes the email: accounts flagged as high-value are held in a rep review queue, all other accounts are sent automatically via Gmail. After send or queue, the agent logs a HubSpot activity record and posts a Slack alert to the assigned account owner.

Trigger
Fires after the Offer Selection Agent returns a confirmed offer_selection object with a score at or above 40.
Tools
HubSpot (contact name, account owner, activity log write), Gmail (send or draft queue)
Replaces steps
Step 5: Draft Personalised Outreach Email; Step 6: Log Outreach in CRM; Step 7: Notify Account Owner in Slack
Estimated build
10 hours, Moderate complexity
// Input
offer_selection: {
  selected_offer_id: string,
  selected_offer_name: string,
  offer_rationale: string,
  account_tier: 'standard' | 'high-value',
  contact_id: string,
  company_id: string,
  customer_id: string
}
hubspot_contact: {
  first_name: string,
  last_name: string,
  email: string,
  account_owner_email: string,
  account_owner_slack_id: string,
  company_name: string
}
tone_guidelines: string        // loaded from config; plain-language, consultative, no pressure language

// Output
email_draft: {
  to: string,                  // contact email address
  subject: string,
  body_html: string,
  body_plain: string
}
routing_decision: 'auto_send' | 'rep_review_queue'
hubspot_activity: {
  contact_id: string,
  activity_type: 'upsell_automation',
  offer_id: string,
  offer_name: string,
  send_timestamp: string,      // ISO 8601
  routing: 'auto_send' | 'rep_review_queue'
}
slack_alert: {
  recipient_slack_id: string,
  message: string,             // includes offer_name and HubSpot contact URL
  hubspot_contact_url: string
}
sheets_row: {
  customer_name: string,
  offer_name: string,
  send_date: string,
  account_owner: string,
  routing: string,
  outcome: 'sent' | 'queued_for_review'
}

// On approval (high-value accounts routed to rep review queue)
rep_approves: Gmail.send(email_draft)
rep_rejects: log_rejection_reason -> sheets_row.outcome = 'rep_rejected'
  • Implementation note: High-value threshold is determined by the account_tier field set in HubSpot. The value 'high-value' triggers rep_review_queue routing. This field must be populated and maintained by the sales team in HubSpot before go-live. Confirm field name and values during integration setup.
  • Implementation note: Email drafting uses a prompt template stored as a configurable variable in the orchestration layer. The template must include placeholders for first_name, company_name, offer_name, and offer_rationale. The prompt instructs the model to keep the email under 150 words and avoid promotional language.
  • Implementation note: Gmail send must use OAuth 2.0 with the gmail.send scope authenticated under a shared sales automation service account, not an individual rep account. Confirm the service account email address with the sales team before build.
  • Implementation note: HubSpot activity log write requires the engagements API (POST /crm/v3/objects/emails). Confirm that the HubSpot account has API access enabled and that the connected app has the crm.objects.contacts.write and crm.objects.emails.write scopes granted.
  • Implementation note: Slack alert uses the Slack Web API chat.postMessage method targeting the account_owner_slack_id stored on the HubSpot contact record. If account_owner_slack_id is null, fall back to posting to a configured default sales channel and log a missing_slack_id warning.
  • Implementation note: Google Sheets append uses the Sheets API v4 spreadsheets.values.append method. The target sheet ID and tab name must be set as environment variables. Confirm the service account has Editor access to the tracking sheet.
  • Implementation note: Rep review queue is implemented as a Gmail draft in the service account inbox plus a Slack message to the rep with an approve/reject link. The review link must deep-link to the HubSpot contact record. A 48-hour inactivity timeout should log a 'review_expired' outcome to the tracking sheet.
Developer Handover PackPage 2 of 4
FS-DOC-04Technical

03End-to-end data flow

Full trigger-to-output data flow with agent handoffs and exact field names at each stage
// ============================================================
// UPSELL & CROSS-SELL TRIGGERS: END-TO-END DATA FLOW
// ============================================================

// TRIGGER: Segment qualifying event received
Segment.track({
  event_name: 'purchase_completed' | 'usage_threshold_crossed' | 'renewal_approaching',
  customer_id: '<segment_user_id>',
  event_properties: {
    spend_total_usd: <number>,
    product_ids_active: ['<product_id>', ...],
    usage_metric_value: <number>,
    renewal_date_iso: '<YYYY-MM-DD>' | null
  },
  timestamp: '<ISO8601>'
})
  -> orchestration_layer.webhook_receiver
     fields_passed: [customer_id, event_name, event_properties, timestamp]

// STEP 1: Fetch account data from HubSpot
orchestration_layer.http_request(
  GET https://api.hubapi.com/crm/v3/objects/contacts/search
  filter: { property: 'segment_user_id', value: customer_id }
  properties: [contact_id, company_id, firstname, lastname, email,
               account_tier, account_owner_email, account_owner_slack_id,
               hs_lead_status, suppression_flag]
)
  -> hubspot_record: { contact_id, company_id, account_tier, open_deals,
                       last_outreach_date, suppression_flag, ... }

// ELIGIBILITY CHECK (pre-agent gate)
if hubspot_record.suppression_flag == true
  OR open_deal.stage IN ['proposal', 'negotiation', 'closed_won', 'closed_lost']:
    log_to_error_table({ reason: 'suppressed_or_open_deal', contact_id, timestamp })
    -> EXIT

// DEDUPE CHECK
orchestration_layer.http_request(
  GET https://api.hubapi.com/crm/v3/objects/emails/search
  filter: { contact_id, activity_type: 'upsell_automation',
            created_after: now() - 30_days }
)
if result.total > 0:
    log_to_error_table({ reason: 'duplicate_suppressed', contact_id, timestamp })
    -> EXIT

// HANDOFF 1: Orchestration -> Offer Selection Agent
OfferSelectionAgent.run({
  segment_event: { event_name, customer_id, event_properties, timestamp },
  hubspot_record: { contact_id, company_id, account_tier, ... },
  offer_catalogue: GoogleSheets.read(sheet_id, tab: 'offer_catalogue')
    // fields: offer_id, offer_name, min_spend_usd, eligible_product_ids,
    //         account_segments, active
})

// OFFER SELECTION AGENT: scoring and output
scoring: {
  spend_tier_score:      (spend_total_usd >= offer.min_spend_usd) -> 0-40,
  product_overlap_score: (product_ids_active intersect eligible_product_ids) -> 0-35,
  usage_proximity_score: (usage_metric_value / threshold_target) -> 0-25
}
composite_score = spend_tier_score + product_overlap_score + usage_proximity_score
if composite_score < 40:
    GoogleSheets.append(tracking_sheet, { reason: 'below_score_threshold', contact_id })
    -> EXIT
selected_offer = offer_catalogue.filter(active == true).sortByScore().first()

// HANDOFF 2: Offer Selection Agent -> Outreach Drafting Agent
OutreachDraftingAgent.run({
  offer_selection: {
    selected_offer_id, selected_offer_name, offer_rationale,
    score, account_tier, contact_id, company_id, customer_id
  },
  hubspot_contact: {
    first_name, last_name, email, account_owner_email,
    account_owner_slack_id, company_name
  },
  tone_guidelines: config.email_tone_prompt
})

// OUTREACH DRAFTING AGENT: draft generation
email_draft = LLM.generate({
  prompt_template: config.email_prompt,
  variables: { first_name, company_name, offer_name, offer_rationale }
  constraints: { max_words: 150, tone: 'consultative', no_promotional_language: true }
})
  -> email_draft: { to, subject, body_html, body_plain }

// ROUTING DECISION
if hubspot_contact.account_tier == 'high-value':
    Gmail.createDraft(email_draft)   // held in service account drafts folder
    Slack.postMessage({
      channel: account_owner_slack_id,
      text: 'Review required: upsell draft for ' + company_name,
      actions: [approve_link, reject_link]   // deep-links to HubSpot contact
    })
    routing_decision = 'rep_review_queue'
else:
    Gmail.send(email_draft)
    routing_decision = 'auto_send'

// STEP: Log activity in HubSpot
orchestration_layer.http_request(
  POST https://api.hubapi.com/crm/v3/objects/emails
  body: {
    hs_timestamp: send_timestamp,
    hs_email_direction: 'EMAIL',
    hs_email_status: 'SENT' | 'DRAFT',
    hs_email_subject: email_draft.subject,
    associations: [{ contact_id, activity_type: 'upsell_automation',
                     offer_id: selected_offer_id, offer_name: selected_offer_name }]
  }
)

// STEP: Post Slack alert to account owner
Slack.postMessage({
  channel: account_owner_slack_id | config.default_sales_channel,
  text: 'Upsell outreach sent: ' + selected_offer_name
        + ' | ' + company_name
        + ' | ' + hubspot_contact_url,
  fallback: log_warning('missing_slack_id', contact_id)
})

// STEP: Append row to Google Sheets tracking tab
GoogleSheets.append(sheet_id, tab: 'upsell_tracking', row: {
  customer_name:  first_name + ' ' + last_name,
  company_name:   company_name,
  offer_name:     selected_offer_name,
  send_date:      send_timestamp,
  account_owner:  account_owner_email,
  routing:        routing_decision,
  outcome:        'sent' | 'queued_for_review'
})

// ============================================================
// HANDOFF 3 (conditional): Rep approves high-value draft
// rep_approves -> Gmail.send(email_draft)
//              -> sheets_row.outcome = 'sent_after_approval'
// rep_rejects  -> log rejection_reason
//              -> sheets_row.outcome = 'rep_rejected'
// timeout(48h) -> sheets_row.outcome = 'review_expired'
// ============================================================
Developer Handover PackPage 3 of 4
FS-DOC-04Technical

04Recommended build stack

Orchestration layer
A workflow automation platform with one workflow per agent plus a shared credential store. The recommended structure is three discrete workflows: (1) Trigger and eligibility check, (2) Offer Selection Agent, (3) Outreach Drafting Agent and downstream actions. Credentials for HubSpot, Gmail, Slack, Google Sheets, and Segment are stored once in the shared credential store and referenced by workflow ID, not duplicated per workflow.
Webhook configuration
Segment forwards qualifying events to the orchestration layer via a Segment Destination Function or a Segment HTTP destination pointed at the orchestration webhook URL. The webhook endpoint should validate a shared secret header (X-Segment-Signature) on every inbound request. The endpoint returns HTTP 200 immediately and processes asynchronously to avoid Segment retry storms. Retry policy on the orchestration side: 3 attempts with exponential back-off (30 s, 2 min, 10 min).
Templating approach
Email body generation uses a stored prompt template retrieved from a config table or environment variable at runtime. The template is parameterised with five substitution tokens: {{first_name}}, {{company_name}}, {{offer_name}}, {{offer_rationale}}, and {{account_owner_name}}. Tone guidelines (consultative, plain-language, under 150 words, no promotional pressure language) are injected as a system prompt prefix. The LLM call should use a temperature of 0.4 to keep output consistent across runs. Output is rendered to both body_html and body_plain before the send or draft step.
Error logging
All exceptions, suppression events, dedupe hits, and no-match exits write a structured row to a Supabase error_log table with columns: event_id (UUID), contact_id, customer_id, workflow_step, error_code, error_detail, and created_at (timestamp). Codes used: suppressed_or_open_deal, duplicate_suppressed, below_score_threshold, missing_slack_id, hubspot_write_failed, gmail_send_failed, sheets_append_failed. A Supabase database webhook fires a Slack alert to the build monitoring channel when error_code is hubspot_write_failed or gmail_send_failed. All other codes are reviewed in the weekly ops review.
Testing approach
All agent logic is validated in sandbox environments first: HubSpot sandbox account, Gmail test mailbox (not a live rep inbox), a Slack test workspace channel, and a staging Google Sheet. Segment events are replayed using saved test event payloads before any live event source is connected. Offer Selection Agent is validated against a set of at least 10 historical customer records with known expected outputs agreed by the sales team. Outreach Drafting Agent output is reviewed by the process owner for tone and personalisation quality before the routing logic is activated. End-to-end testing runs across at least 5 full event cycles before the pilot opens.
Estimated total build time
Offer Selection Agent: 10 hours. Outreach Drafting Agent: 10 hours. End-to-end integration, webhook setup, error logging, routing logic, and QA: 4 hours. Total: 24 hours across the build period.
Before build begins, the following must be confirmed: (1) HubSpot Sales Hub tier supports the engagements and emails API endpoints required. (2) Gmail service account is created with gmail.send OAuth scope and shared with the FullSpec team. (3) Segment event names for all three qualifying trigger conditions are finalised and match the taxonomy in this document exactly. (4) The offer catalogue Google Sheet is populated and follows the schema defined in the Offer Selection Agent spec. (5) The high-value account threshold value is agreed by the sales team and set in HubSpot as the account_tier field. Any of these items unresolved at build start will cause delays.
For build support or questions about this handover pack, contact the FullSpec team at support@gofullspec.com.
Developer Handover PackPage 4 of 4

More documents for this process

Every document generated for Upsell & Cross-sell Triggers.

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