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
Quote Generation & Follow-up
[YourCompany.com] · Sales Department · Prepared by FullSpec · [Today's Date]
This document is the authoritative technical reference for the Quote Generation & Follow-up automation build. It covers the full current-state process map with bottleneck flags, per-agent specifications with IO contracts, the end-to-end data flow trace, and the recommended build stack. The FullSpec team uses this pack to build, wire, and test every component. Where a decision or confirmation is still outstanding before build can proceed, it is marked explicitly in the relevant agent specification.
01Process snapshot
02Agent specifications
Reads deal and contact data from the HubSpot opportunity record, fetches the correct pricing rows from the master Google Sheets pricing table using a structured lookup, and populates the approved PandaDoc quote template with client details, line items, pricing, applicable discounts, and expiry date. The agent applies the configured discount threshold rule and flags any line items outside standard pricing bands before marking the document as ready for review. No rep involvement is required at any point in this process. Estimated build time: 12 hours. Complexity: Moderate.
// Input hubspot.deal.id : string // e.g. '123456789' hubspot.deal.name : string hubspot.deal.amount : number hubspot.deal.discount_pct : number // 0-100, sourced from hs_discount_percent property hubspot.contact.firstname : string hubspot.contact.lastname : string hubspot.contact.email : string hubspot.contact.company : string hubspot.line_items[] : array // product_id, quantity, unit_price // Sheets lookup result sheets.row.product_id : string // matched against hubspot.line_items[].product_id sheets.row.product_name : string sheets.row.unit_price : number sheets.row.standard_band_min : number sheets.row.standard_band_max : number // Output pandadoc.document.id : string // newly created draft document ID pandadoc.document.status : string // expected: 'document.draft' pandadoc.document.url : string // direct link for manager review agent.flag.outside_band : boolean // true if any line item falls outside standard band agent.flag.approval_required : boolean // true if discount_pct >= 15
- HubSpot tier must be at least Sales Hub Starter for webhook access. Confirm the active subscription tier before build starts.
- The Google Sheets pricing table must be formatted as a flat lookup table with a header row and no merged cells. The column named 'product_id' must match the HubSpot product ID exactly (string comparison, case-sensitive). Any freeform or merged-cell spreadsheet must be reformatted before the agent can read it.
- The Sheets API service account must be granted 'Viewer' access to the pricing spreadsheet. Store the service account JSON credential in the shared credential store under the key GSHEETS_SERVICE_ACCOUNT.
- PandaDoc template variable keys must map exactly to the field names listed in the IO block above. Any mismatch produces a blank field in the generated document. Confirm the template variable list with the process owner before build.
- The discount threshold for mandatory approval is 15%. This value must be stored as a workflow-level environment variable (APPROVAL_THRESHOLD_PCT) so it can be adjusted without a rebuild.
- If the Sheets lookup returns no matching row for a product_id, the agent must halt the workflow and post an error alert (see Section 04 for logging details) rather than generating a document with a blank price.
- Dedupe behaviour: if a HubSpot deal already has an associated PandaDoc document ID stored in the custom property pandadoc_document_id, the agent must skip document creation and log a warning to avoid duplicate drafts.
- PandaDoc document expiry date is calculated as today plus 14 days by default. Confirm this value with the process owner before build.
Receives the PandaDoc draft document ID from the Quote Builder Agent, evaluates whether manager approval is required based on the discount threshold flag, and routes accordingly. If approval is required, it posts a structured Slack message to the designated sales-manager channel containing the quote value, discount percentage, and a direct PandaDoc review link, then waits for a structured button response. On approval it sends the finalised quote to the prospect via Gmail using a personalised cover message built from HubSpot contact data, then writes the send details back to the HubSpot opportunity record. On rejection it posts a revision note to the sales channel and halts the workflow pending manual correction. If no approval is required, the agent proceeds directly to the Gmail send step. Estimated build time: 10 hours. Complexity: Moderate.
// Input pandadoc.document.id : string pandadoc.document.url : string agent.flag.approval_required : boolean hubspot.deal.id : string hubspot.deal.amount : number hubspot.deal.discount_pct : number hubspot.contact.firstname : string hubspot.contact.email : string hubspot.contact.company : string // On approval (Slack interactive payload) slack.action.value : string // 'approve' or 'reject' slack.user.id : string // approving manager's Slack user ID slack.message.ts : string // message timestamp for update // Output (on approval path) gmail.message.id : string // sent message ID gmail.message.thread_id : string hubspot.deal.stage : string // written as 'Quote Sent' hubspot.deal.quote_sent_date : string // ISO 8601 date hubspot.deal.pandadoc_document_id : string // stored for downstream agents // Output (on rejection path) slack.notification.channel : string // sales-team channel workflow.status : string // 'halted_pending_revision'
- The Slack approval channel name must be confirmed before build. The channel ID (not display name) must be stored as the environment variable SLACK_APPROVAL_CHANNEL_ID. The fallback notification channel for rejections must be stored as SLACK_SALES_CHANNEL_ID.
- Slack interactive messages require a Slack App with the chat:write and chat:update OAuth scopes, plus an incoming webhook URL. The app must be installed to the workspace and the bot added to both channels before build starts.
- The Slack approval message must use Block Kit with two action buttons labelled 'Approve' and 'Request Changes'. The button values must be exactly the strings 'approve' and 'reject' for the conditional branch to resolve correctly.
- Approval timeout is set to 24 hours. If no Slack response is received within 24 hours, the agent must escalate by posting a reminder to the sales manager via Slack direct message and restarting the 24-hour window once. After a second timeout, the workflow halts and logs an error.
- Gmail sends must use the OAuth 2.0 service account delegated to the sales rep's Gmail address (stored as GMAIL_SENDER_ADDRESS). The cover message template is stored as a workflow-level variable and uses HubSpot contact fields for personalisation tokens.
- The HubSpot deal property pandadoc_document_id must be created as a custom single-line text property before build. Confirm the internal property name with the HubSpot admin.
- If agent.flag.approval_required is false, the agent skips the Slack step entirely and proceeds directly to the Gmail send, ensuring sub-15-minute turnaround for standard quotes.
- PandaDoc document must be moved to 'document.sent' status via the PandaDoc API after Gmail delivery to keep PandaDoc and HubSpot statuses in sync.
Monitors the PandaDoc document for open and signing events via webhook. If the document has not been signed after 3 days from send, the agent dispatches the first follow-up email through Gmail. If still unsigned after 7 days, it dispatches a second follow-up. On receipt of a PandaDoc 'document.completed' event (prospect signed), the agent creates a matching draft invoice in Xero using the line items and values from the PandaDoc document, then updates the HubSpot deal stage to Won. If the document reaches its expiry date without a signing event, the agent updates the HubSpot stage to Lost and sends a brief internal notification to the rep via Slack. The agent cancels any pending follow-up steps as soon as a terminal event is received to prevent emails firing after a deal is already closed. Estimated build time: 10 hours. Complexity: Moderate.
// Input hubspot.deal.id : string hubspot.deal.quote_sent_date : string // ISO 8601 hubspot.deal.pandadoc_document_id : string hubspot.contact.email : string hubspot.contact.firstname : string // PandaDoc webhook events (inbound) pandadoc.event.type : string // 'document.viewed' | 'document.completed' | 'document.expired' pandadoc.event.document_id : string pandadoc.event.recipients[].email : string pandadoc.line_items[].name : string pandadoc.line_items[].quantity : number pandadoc.line_items[].unit_price : number pandadoc.line_items[].total : number // Output (follow-up path) gmail.followup_1.message_id : string // sent at day 3 if unsigned gmail.followup_2.message_id : string // sent at day 7 if unsigned // Output (acceptance path) xero.invoice.id : string // draft invoice ID xero.invoice.status : string // expected: 'DRAFT' xero.invoice.line_items : array // mirrored from pandadoc.line_items hubspot.deal.stage : string // written as 'closedwon' // Output (expiry path) hubspot.deal.stage : string // written as 'closedlost' slack.notification.sent : boolean // rep notified in sales channel
- PandaDoc webhook endpoint must be registered in the PandaDoc developer console to receive document.viewed, document.completed, and document.expired events. The endpoint URL is provided by the orchestration layer after deployment. Store the PandaDoc webhook secret as PANDADOC_WEBHOOK_SECRET and validate the HMAC-SHA256 signature on every inbound request.
- Day-3 and day-7 follow-up timers must be implemented as scheduled workflow steps anchored to hubspot.deal.quote_sent_date, not to wall-clock time of workflow execution, to survive any platform restart between send and follow-up dates.
- Both follow-up email templates must be stored as workflow-level variables. Template tokens are hubspot.contact.firstname and hubspot.deal.name. Confirm final copy with the process owner before go-live.
- Xero invoice creation requires the Xero API with the accounting.transactions scope. The Xero contact must be matched by hubspot.contact.email against existing Xero contacts. If no match is found, the agent must create a new Xero contact before raising the invoice. Store the Xero OAuth 2.0 refresh token as XERO_REFRESH_TOKEN.
- If a document.completed event arrives before the day-3 timer fires, the agent must cancel the scheduled follow-up steps immediately to prevent emails being sent to a prospect who has already signed.
- HubSpot deal stage internal values for Won and Lost must be confirmed with the HubSpot admin. Do not assume 'closedwon' and 'closedlost' are the correct pipeline stage IDs without verification.
- Dedupe protection: if the agent receives more than one document.completed event for the same document_id (PandaDoc may send duplicates), it must check whether a Xero invoice already exists for the deal before creating a second one. Use hubspot.deal.xero_invoice_id (custom property) as the idempotency key.
03End-to-end data flow
// ============================================================
// QUOTE GENERATION & FOLLOW-UP | End-to-end data flow
// ============================================================
// TRIGGER
// HubSpot deal stage property 'dealstage' changes to 'Quote Requested'
// Orchestration layer webhook receives POST from HubSpot
INBOUND hubspot.webhook {
deal.id : '123456789'
deal.name : 'Acme Corp - Managed Service Q2'
deal.amount : 8400.00
deal.discount_pct : 18 // triggers approval_required = true
deal.hs_object_id : '123456789'
contact.id : '987654321'
contact.firstname : 'Jane'
contact.lastname : 'Smith'
contact.email : 'jane.smith@acmecorp.com'
contact.company : 'Acme Corp'
line_items[] : [{ product_id: 'SKU-001', quantity: 3, unit_price: 2800.00 }]
}
// ============================================================
// AGENT HANDOFF 1: Orchestration layer -> Quote Builder Agent
// ============================================================
STEP 1: Google Sheets pricing lookup
REQUEST sheets.spreadsheet_id = GSHEETS_PRICING_ID
FILTER sheets.row WHERE product_id = 'SKU-001'
RETURNS {
product_name : 'Managed Service - Standard'
unit_price : 2800.00
standard_band_min : 2500.00
standard_band_max : 3200.00
}
EVALUATE unit_price WITHIN band -> flag.outside_band = false
EVALUATE discount_pct (18) >= APPROVAL_THRESHOLD_PCT (15) -> flag.approval_required = true
STEP 2: PandaDoc document creation
REQUEST POST /public/v1/documents
BODY {
template_uuid : PANDADOC_TEMPLATE_UUID
name : 'Quote - Acme Corp - 2024-04-14'
recipients : [{ email: 'jane.smith@acmecorp.com', first_name: 'Jane', last_name: 'Smith', role: 'Signer' }]
tokens : [
{ name: 'client.name', value: 'Acme Corp' }
{ name: 'client.firstname', value: 'Jane' }
{ name: 'deal.amount', value: '8400.00' }
{ name: 'deal.discount_pct', value: '18' }
{ name: 'quote.expiry_date', value: '2024-04-28' }
]
pricing.tables[] : [{ product_id: 'SKU-001', name: 'Managed Service - Standard', qty: 3, price: 2800.00 }]
}
RETURNS {
document.id : 'pd_doc_abc123'
document.status : 'document.draft'
document.url : 'https://app.pandadoc.com/d/pd_doc_abc123'
}
WRITE hubspot.deal.pandadoc_document_id = 'pd_doc_abc123'
// ============================================================
// AGENT HANDOFF 2: Quote Builder Agent -> Approval and Send Agent
// ============================================================
STEP 3: Slack approval routing (flag.approval_required = true)
REQUEST POST slack.chat.postMessage
CHANNEL SLACK_APPROVAL_CHANNEL_ID
BLOCK_KIT {
text : 'Quote ready for approval: Acme Corp | $8,400 | 18% discount'
actions : [ { text: 'Approve', value: 'approve' }, { text: 'Request Changes', value: 'reject' } ]
url : 'https://app.pandadoc.com/d/pd_doc_abc123'
}
AWAIT slack.interactive.payload TIMEOUT 24h
RECEIVE {
slack.action.value : 'approve'
slack.user.id : 'U0123MANAGER'
slack.message.ts : '1712840000.000100'
}
UPDATE slack message -> 'Approved by [Manager] at 14:23'
STEP 4: Gmail quote send
REQUEST gmail.users.messages.send
FROM GMAIL_SENDER_ADDRESS
TO 'jane.smith@acmecorp.com'
SUBJECT 'Your quote from [YourCompany.com] is ready, Jane'
BODY personalised_cover_template + pandadoc.document.url
RETURNS {
gmail.message.id : 'msg_xyz789'
gmail.thread_id : 'thread_abc456'
}
STEP 5: HubSpot deal update (post-send)
REQUEST PATCH /crm/v3/objects/deals/123456789
PROPERTIES {
dealstage : 'Quote Sent'
quote_sent_date : '2024-04-14T14:23:00Z'
pandadoc_document_id : 'pd_doc_abc123'
}
// ============================================================
// AGENT HANDOFF 3: Approval and Send Agent -> Follow-up and Close Agent
// ============================================================
STEP 6: Schedule follow-up timers anchored to quote_sent_date
TIMER_1 fire at quote_sent_date + 3 days IF document.status != 'document.completed'
TIMER_2 fire at quote_sent_date + 7 days IF document.status != 'document.completed'
STEP 7a: Day-3 follow-up email (if no signing event received)
REQUEST gmail.users.messages.send
FROM GMAIL_SENDER_ADDRESS
TO 'jane.smith@acmecorp.com'
SUBJECT 'Just checking in, Jane - your quote is still available'
BODY followup_template_1 (token: contact.firstname, deal.name)
RETURNS gmail.followup_1.message_id : 'msg_fu1_001'
STEP 7b: Day-7 follow-up email (if still no signing event)
REQUEST gmail.users.messages.send
TO 'jane.smith@acmecorp.com'
SUBJECT 'Your quote expires soon, Jane'
BODY followup_template_2 (token: contact.firstname, quote.expiry_date)
RETURNS gmail.followup_2.message_id : 'msg_fu2_002'
// PandaDoc webhook event received
INBOUND pandadoc.webhook { event.type: 'document.completed', document_id: 'pd_doc_abc123' }
CANCEL pending timers for document_id 'pd_doc_abc123'
STEP 8: Xero invoice creation
LOOKUP xero.contacts WHERE email = 'jane.smith@acmecorp.com'
RETURNS xero.contact.id = 'xero_contact_789'
REQUEST POST /api.xro/2.0/Invoices
BODY {
Type : 'ACCREC'
Status : 'DRAFT'
Contact.ContactID : 'xero_contact_789'
LineItems[] : [{ Description: 'Managed Service - Standard', Quantity: 3, UnitAmount: 2800.00, AccountCode: '200' }]
DueDate : quote_sent_date + 30 days
}
RETURNS xero.invoice.id = 'INV-0042'
WRITE hubspot.deal.xero_invoice_id = 'INV-0042'
STEP 9: HubSpot stage close
REQUEST PATCH /crm/v3/objects/deals/123456789
PROPERTIES { dealstage: 'closedwon' }
// ============================================================
// EXPIRY PATH (alternative to acceptance)
// PandaDoc webhook: event.type = 'document.expired'
// ============================================================
INBOUND pandadoc.webhook { event.type: 'document.expired', document_id: 'pd_doc_abc123' }
CANCEL pending timers
WRITE hubspot.deal.dealstage = 'closedlost'
NOTIFY slack.sales_channel -> 'Quote for Acme Corp has expired. Deal moved to Lost.'04Recommended build stack
More documents for this process
Every document generated for Quote Generation & Follow-up.