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
Pricing Approval Workflow
[YourCompany.com] · Sales Department · Prepared by FullSpec · [Today's Date]
This document is the primary technical reference for the FullSpec team building the Pricing Approval Workflow automation. It covers the current-state process map with identified bottlenecks, full specifications for each of the three agents, the end-to-end data flow across all integrated tools, and the recommended build stack. All build decisions, field names, and configuration values in this document supersede earlier notes. The process owner and stakeholders have agreed the scope described here; any changes to discount thresholds, HubSpot property names, or approval logic must be reflected in this document before build proceeds.
01Process snapshot
02Agent specifications
Monitors HubSpot for deal records where a pricing exception has been raised, extracts all required fields, validates that no mandatory values are missing, and writes a structured row to the Google Sheets approval log. This agent is the entry point to the entire workflow. It must deduplicate incoming triggers to prevent the same deal generating multiple log entries, and it must surface a clear validation error to the submitting rep if required fields are absent rather than silently failing. Build complexity is Moderate given the field validation logic and the HubSpot webhook configuration required.
// Input (from HubSpot webhook payload) deal_id: string // HubSpot internal deal ID deal_name: string // Deal display name rep_email: string // Owner email from deal record deal_value_usd: number // Deal amount field requested_discount_pct: number // Custom property: pricing_exception_discount_pct exception_reason: string // Custom property: pricing_exception_reason standard_price_usd: number // Custom property: standard_list_price requested_price_usd: number // Derived: standard_price_usd * (1 - requested_discount_pct/100) gross_margin_impact: string // Custom property: pricing_exception_margin_note submitted_at: ISO8601 string // Timestamp of webhook fire // Validation checks before log write REQUIRED: deal_id, deal_name, rep_email, deal_value_usd, requested_discount_pct, exception_reason DEDUPE: query Google Sheets log for existing row where deal_id matches and status != 'Closed' If duplicate found: suppress new row, send Slack DM to rep_email confirming existing request is open If validation fails: send Slack DM to rep_email listing missing fields, halt workflow // Output (written to Google Sheets: tab 'Approval Log', next empty row) log_row_id: string // UUID generated at write time deal_id: string deal_name: string rep_email: string deal_value_usd: number requested_discount_pct: number requested_price_usd: number exception_reason: string gross_margin_impact: string submitted_at: ISO8601 string status: 'Pending' // Initial status value approver_email: null // Populated by Routing Agent decision: null // Populated by Routing Agent decision_timestamp: null // Populated by Routing Agent approver_comment: null // Populated by Routing Agent
- HubSpot custom properties must be created before build starts: 'pricing_exception_requested' (boolean), 'pricing_exception_discount_pct' (number), 'pricing_exception_reason' (single-line text), 'standard_list_price' (number), 'pricing_exception_margin_note' (single-line text), 'approved_price_usd' (number), 'pricing_exception_status' (dropdown: Pending, Approved, Denied), 'approver_name' (single-line text), 'approval_date' (date). Confirm exact property internal names with the HubSpot admin before build.
- HubSpot plan must be at least Starter tier to enable webhook subscriptions on property changes. Confirm this with [YourCompany.com] before build commences.
- Google Sheets tab must be named exactly 'Approval Log' with column headers matching the output field names above in row 1. A second tab named 'Config' will store discount tier thresholds as named ranges for the Routing Agent to read.
- Deduplication logic checks for an open row (status != 'Closed') with a matching deal_id before writing. A deal that was previously closed and reopened will generate a new log row; this is intentional.
- If a required field is missing, the agent sends a Slack DM to the rep's email (matched against Slack workspace members) and halts. It does not write a partial row to the log.
- Confirm with the sales manager and finance lead whether 'Closed' status covers both Approved and Denied outcomes, or whether Denied rows should remain queryable separately. This affects the dedupe condition.
Reads new Pending rows from the Google Sheets approval log, determines the correct approval path by comparing the requested discount percentage against configured tier thresholds, and dispatches a structured one-click approval request via Slack for standard approvals or Gmail for finance escalations. The agent polls the log on a defined schedule and monitors for responses, firing reminder messages if no decision is received within the configured window. When a decision arrives via Slack interactive callback or Gmail link click, the agent writes the outcome back to the log row and updates the deal status field in HubSpot to signal the CRM Update Agent. This is the most complex agent in the build due to the branching escalation logic, reminder cadence, and the Slack interactive component requirement.
// Input (read from Google Sheets 'Approval Log' row where status = 'Pending')
log_row_id: string
deal_id: string
deal_name: string
rep_email: string
deal_value_usd: number
requested_discount_pct: number
requested_price_usd: number
exception_reason: string
gross_margin_impact: string
submitted_at: ISO8601 string
// Tier logic (thresholds read from Google Sheets 'Config' tab, named ranges)
TIER_1_MAX_PCT: number // e.g. 10 — manager-only approval
TIER_2_MAX_PCT: number // e.g. 20 — manager approves, finance notified
TIER_3_THRESHOLD_PCT: number // e.g. 21+ — finance approver required, manager cc'd
REMINDER_HOURS: number // e.g. 4 — hours before first reminder fires
ESCALATION_HOURS: number // e.g. 24 — hours before escalation to next approver
// Slack approval message fields (sent to manager Slack user ID, looked up by rep_email match)
slack_channel: DM to approver_slack_user_id
message_blocks: deal_name, deal_value_usd, requested_discount_pct, requested_price_usd,
exception_reason, gross_margin_impact, approve_button, deny_button
callback_id: 'pricing_approval_' + log_row_id
// Gmail escalation fields (sent when requested_discount_pct >= TIER_3_THRESHOLD_PCT)
to: finance_approver_email // Read from 'Config' tab named range FINANCE_EMAIL
cc: manager_email // Read from 'Config' tab named range MANAGER_EMAIL
subject: 'Finance Approval Required: ' + deal_name
body_template: structured HTML with deal_name, requested_discount_pct, deal_value_usd,
requested_price_usd, exception_reason, approve_url, deny_url
approve_url: webhook endpoint + '?action=approve&row=' + log_row_id + '&token=' + signed_token
deny_url: webhook endpoint + '?action=deny&row=' + log_row_id + '&token=' + signed_token
// On approval or denial (Slack callback or Gmail link click)
decision: 'Approved' | 'Denied'
approver_email: string // Extracted from Slack payload or URL token
approver_name: string // Looked up from Slack profile or Config tab
approver_comment: string // Optional; from Slack modal or URL param
decision_timestamp: ISO8601 string
// Output (written back to Google Sheets 'Approval Log' row, matched by log_row_id)
status: 'Approved' | 'Denied'
approver_email: string
approver_name: string
approver_comment: string
decision_timestamp: ISO8601 string
// Output (written to HubSpot deal property to signal CRM Update Agent)
pricing_exception_status: 'Approved' | 'Denied'- A Slack app must be created in the [YourCompany.com] Slack workspace with the following scopes: chat:write, im:write, users:read, users:read.email, and interactive components enabled. A workspace admin must approve the app installation before build starts.
- The Slack interactive component callback URL must point to a stable webhook endpoint hosted within the automation platform. Confirm the platform can expose a public HTTPS endpoint for Slack callbacks before build.
- Approval link tokens for Gmail escalations must be signed using HMAC-SHA256 with a secret stored in the credential store. Tokens must expire after 72 hours. If the link is clicked after expiry the agent must return a clear error page and log the attempt.
- Tier thresholds (TIER_1_MAX_PCT, TIER_2_MAX_PCT, TIER_3_THRESHOLD_PCT), reminder timing (REMINDER_HOURS, ESCALATION_HOURS), finance approver email, and manager email must all be stored as named ranges in the 'Config' tab, not hardcoded in the workflow. This allows threshold changes without touching the automation.
- The reminder logic must track whether a reminder has already been sent for a given log_row_id to prevent repeated reminders. Store a 'reminder_sent_at' field in the log row.
- If the Slack user ID lookup by rep_email fails (the rep is not in the Slack workspace), fall back to sending the approval request via Gmail to manager_email. Log the fallback event to the error table.
- Confirm with the sales manager whether a 'deny with comment' flow is required in Slack (modal popup) or whether free-text comment is optional. This affects the Slack block kit design and callback handling.
Monitors the Google Sheets approval log for rows where a final decision has been recorded and the corresponding HubSpot deal property 'pricing_exception_status' has been updated to Approved or Denied. On detecting a completed decision, the agent writes the approved price, decision date, approver name, and approval status back to the HubSpot deal record properties, then sends a Slack direct message to the sales rep confirming the outcome with all relevant details. This agent is the final automated step in the workflow; it has no branching logic and is the simplest of the three agents.
// Input (from HubSpot webhook: pricing_exception_status property change)
deal_id: string
pricing_exception_status: 'Approved' | 'Denied'
// Lookup (read from Google Sheets 'Approval Log', matched by deal_id, status != Pending)
log_row_id: string
deal_name: string
rep_email: string
requested_price_usd: number
approved_price_usd: number // = requested_price_usd if Approved, else null
approver_name: string
approver_comment: string
decision_timestamp: ISO8601 string
decision: 'Approved' | 'Denied'
// Output (written to HubSpot deal record properties via PATCH /crm/v3/objects/deals/{deal_id})
approved_price_usd: number // Custom property, null if Denied
pricing_exception_status: 'Approved' | 'Denied'
approver_name: string // Custom property
approval_date: date string // Derived from decision_timestamp, format YYYY-MM-DD
// Output (Slack DM to rep, looked up by rep_email against Slack workspace members)
recipient: rep_slack_user_id
message: deal_name + decision + approved_price_usd (if Approved) + approver_name + approver_comment
fallback: if Slack user lookup fails, send Gmail to rep_email with same content- This agent must not fire until the Routing Agent has written a complete decision row to the log. The HubSpot property change trigger is the reliable signal; do not rely on polling the Sheets log alone.
- If the decision is Denied, set approved_price_usd to null (do not write zero) in HubSpot to avoid misleading reporting.
- Slack DM to the rep uses the same user lookup pattern as the Routing Agent. If the lookup fails, fall back to Gmail and log the event.
- The agent must be idempotent: if the HubSpot webhook fires twice for the same deal_id and status, the second execution must detect the deal record already has a decision written and exit without duplicating the Slack DM.
03End-to-end data flow
// ============================================================
// TRIGGER: Rep sets pricing_exception_requested = true on HubSpot deal
// ============================================================
HubSpot webhook fires on property change: pricing_exception_requested -> true
payload fields: deal_id, deal_name, rep_email, deal_value_usd,
pricing_exception_discount_pct, pricing_exception_reason,
standard_list_price, pricing_exception_margin_note, submitted_at
// ============================================================
// AGENT 1: Request Intake Agent
// ============================================================
Step 1.1 — Receive webhook payload from HubSpot
extract: deal_id, deal_name, rep_email, deal_value_usd,
requested_discount_pct, exception_reason, standard_list_price,
gross_margin_impact, submitted_at
derive: requested_price_usd = standard_list_price * (1 - requested_discount_pct / 100)
Step 1.2 — Validate required fields
REQUIRED: deal_id, deal_name, rep_email, deal_value_usd,
requested_discount_pct, exception_reason
IF any field missing:
-> Slack DM to rep_email: 'Your pricing request is missing [field list]. Please update the deal record.'
-> HALT workflow for this deal_id
Step 1.3 — Deduplicate against Google Sheets 'Approval Log'
query: SELECT log_row_id WHERE deal_id = :deal_id AND status != 'Closed'
IF duplicate row found:
-> Slack DM to rep_email: 'A pricing request for [deal_name] is already open (ID: [log_row_id]).'
-> HALT workflow for this deal_id
Step 1.4 — Write structured row to Google Sheets 'Approval Log'
write fields: log_row_id (UUID), deal_id, deal_name, rep_email, deal_value_usd,
requested_discount_pct, requested_price_usd, exception_reason,
gross_margin_impact, submitted_at, status='Pending',
approver_email=null, decision=null, decision_timestamp=null,
approver_comment=null, reminder_sent_at=null
// --- AGENT HANDOFF: Intake Agent -> Approval Routing Agent ---
// Signal: new row with status='Pending' visible in 'Approval Log' tab
// Routing Agent polls tab every 2 minutes; picks up log_row_id
// ============================================================
// AGENT 2: Approval Routing Agent
// ============================================================
Step 2.1 — Read new Pending rows from Google Sheets 'Approval Log'
read: all rows where status = 'Pending' and approver_email IS NULL
Step 2.2 — Read tier thresholds from Google Sheets 'Config' tab
TIER_1_MAX_PCT (e.g. 10)
TIER_2_MAX_PCT (e.g. 20)
TIER_3_THRESHOLD_PCT (e.g. 21)
REMINDER_HOURS (e.g. 4)
ESCALATION_HOURS (e.g. 24)
MANAGER_EMAIL (e.g. manager@yourcompany.com)
FINANCE_EMAIL (e.g. finance@yourcompany.com)
Step 2.3 — Determine approval path
IF requested_discount_pct <= TIER_1_MAX_PCT:
route = 'SLACK_MANAGER_ONLY'
approver_email = MANAGER_EMAIL
ELSE IF requested_discount_pct <= TIER_2_MAX_PCT:
route = 'SLACK_MANAGER_NOTIFY_FINANCE'
approver_email = MANAGER_EMAIL
notify_email = FINANCE_EMAIL
ELSE:
route = 'GMAIL_FINANCE_ESCALATION'
approver_email = FINANCE_EMAIL
cc_email = MANAGER_EMAIL
Step 2.4a — IF route = SLACK_MANAGER_ONLY or SLACK_MANAGER_NOTIFY_FINANCE:
lookup approver Slack user ID by approver_email
IF lookup fails: fall back to Gmail to approver_email, log fallback
send Slack DM with Block Kit message:
fields: deal_name, deal_value_usd, requested_discount_pct, requested_price_usd,
exception_reason, gross_margin_impact
actions: [Approve] button, [Deny] button
callback_id: 'pricing_approval_' + log_row_id
update 'Approval Log' row: approver_email = approver_email, reminder_sent_at = null
Step 2.4b — IF route = GMAIL_FINANCE_ESCALATION:
generate signed token: HMAC-SHA256(log_row_id + action, SECRET_KEY), expires 72 hrs
approve_url = webhook_base + '?action=approve&row=' + log_row_id + '&token=' + signed_token
deny_url = webhook_base + '?action=deny&row=' + log_row_id + '&token=' + signed_token
send Gmail:
to: FINANCE_EMAIL
cc: MANAGER_EMAIL
subject: 'Finance Approval Required: ' + deal_name
body: structured HTML with deal_name, deal_value_usd, requested_discount_pct,
requested_price_usd, exception_reason, gross_margin_impact,
approve_url, deny_url
Step 2.5 — Reminder and escalation monitor (runs on schedule every 30 min)
query 'Approval Log': rows where status='Pending' and decision IS NULL
FOR each row:
IF now - submitted_at >= REMINDER_HOURS AND reminder_sent_at IS NULL:
resend Slack DM (or Gmail) reminder with same callback_id / links
write reminder_sent_at = now to log row
IF now - submitted_at >= ESCALATION_HOURS AND decision IS NULL:
send Slack DM to MANAGER_EMAIL: '[deal_name] pricing request has not been actioned for 24 hours.'
log escalation event to error table
Step 2.6 — Receive decision (Slack callback or Gmail link click)
Slack: action_type (approve|deny) from callback payload, approver_slack_user_id, optional comment
Gmail: action (approve|deny) from URL param, validate signed token (reject if expired)
derive: approver_name (Slack profile lookup or Config tab), decision_timestamp = now
Step 2.7 — Write decision to Google Sheets 'Approval Log' row (matched by log_row_id)
status: 'Approved' | 'Denied'
approver_email, approver_name, approver_comment, decision_timestamp
Step 2.8 — Write signal to HubSpot deal record
PATCH /crm/v3/objects/deals/{deal_id}
properties: { pricing_exception_status: 'Approved' | 'Denied' }
// --- AGENT HANDOFF: Approval Routing Agent -> CRM Update Agent ---
// Signal: HubSpot webhook fires on pricing_exception_status property change
// CRM Update Agent receives deal_id and new status value
// ============================================================
// AGENT 3: CRM Update Agent
// ============================================================
Step 3.1 — Receive HubSpot webhook: pricing_exception_status changed
payload: deal_id, pricing_exception_status ('Approved' | 'Denied')
Step 3.2 — Idempotency check
read HubSpot deal: check if approval_date is already populated
IF yes: log 'duplicate trigger suppressed for deal_id' and EXIT
Step 3.3 — Read decision details from Google Sheets 'Approval Log'
query: SELECT * WHERE deal_id = :deal_id AND status IN ('Approved','Denied')
read: deal_name, rep_email, requested_price_usd, approver_name,
approver_comment, decision_timestamp, decision
derive: approved_price_usd = requested_price_usd IF decision='Approved' ELSE null
approval_date = DATE(decision_timestamp) // format YYYY-MM-DD
Step 3.4 — Write final outcome to HubSpot deal record
PATCH /crm/v3/objects/deals/{deal_id}
properties:
approved_price_usd: number | null
pricing_exception_status: 'Approved' | 'Denied'
approver_name: string
approval_date: YYYY-MM-DD
Step 3.5 — Send Slack DM to rep
lookup rep Slack user ID by rep_email
IF lookup fails: send Gmail to rep_email with same content, log fallback
message content: deal_name, decision, approved_price_usd (if Approved),
approver_name, approver_comment
// ============================================================
// END OF AUTOMATED WORKFLOW
// Remaining manual step: rep updates quote in DocuSign (Step 9, out of scope)
// ============================================================04Recommended build stack
More documents for this process
Every document generated for Pricing Approval Workflow.