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
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
02Agent specifications
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.
// 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.
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.
// 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.
03End-to-end data flow
// ============================================================
// 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'
// ============================================================04Recommended build stack
More documents for this process
Every document generated for Upsell & Cross-sell Triggers.