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
Proposal Creation Automation
[YourCompany.com] · Sales Department · Prepared by FullSpec · [Today's Date]
This document is the authoritative technical reference for the FullSpec team building the Proposal Creation automation. It covers the current-state step map, full agent specifications with IO contracts, the end-to-end data flow, and the recommended build stack. Everything needed to begin implementation without additional discovery is contained here. Where a field name, variable, or rule is marked as requiring confirmation, that item must be resolved before the relevant agent is wired to a live credential.
01Process snapshot
02Agent specifications
Fires on a HubSpot webhook when a deal moves to the 'Proposal Requested' stage. The agent reads all deal properties from HubSpot, resolves the correct pricing tier and any applicable discount rules against the documented pricing logic, validates that all required template fields are present, and creates a fully populated PandaDoc document from the master template. If any required field is missing or unpopulated, the agent halts, logs the error, and posts a Slack notification to the deal owner before exiting. No document is created unless all required fields resolve cleanly. Build complexity is Moderate; the pricing resolution logic and field-validation layer are the primary complexity drivers.
// Input: HubSpot deal webhook payload deal_id : string // HubSpot internal deal ID dealstage : string // must equal 'Proposal Requested' stage ID contact_id : string // associated contact record ID company_id : string // associated company record ID deal_name : string // used as proposal document title deal_value : number // USD, used for pricing tier resolution product_line : string // enum: must match documented tier keys discount_code : string? // optional; validated against discount table contact_firstname : string contact_lastname : string contact_email : string company_name : string proposal_expiry_days : number // default 14 if not set on deal // Output: PandaDoc document object pandadoc_doc_id : string // stored back to HubSpot deal field pandadoc_doc_url : string // preview link passed to Approval and Delivery Agent resolved_unit_price: number // pricing tier output, USD resolved_discount : number // percentage, 0 if none resolved_total : number // final proposal value, USD doc_status : string // 'draft' on success; 'error' if field validation fails missing_fields : string[] // populated only on error; lists field names that blocked generation
- HubSpot tier requirement: the connected HubSpot account must be on Sales Hub Starter or above to expose deal property webhooks. Confirm account tier before configuring the trigger.
- PandaDoc tier requirement: the account must be on the Business tier or above to access the Document Create from Template API endpoint. Confirm template ID and variable schema before build.
- Pricing resolution logic must be fully documented before this agent is built. All product lines, volume bands, and discount rules must exist as a structured reference in the automation platform credential store or an internal lookup table. Do not read from the live Google Sheet at runtime.
- PandaDoc template variable names must match HubSpot deal field API names exactly, or be explicitly mapped in the agent. A mismatch produces a blank field in the generated document with no API error thrown. Validate the full variable list against the confirmed PandaDoc template before wiring.
- Field validation gate: if any of the following fields are null or empty on the incoming deal, the agent must halt and not create a document: contact_email, company_name, deal_value, product_line. Log the missing field names and post a Slack alert to the deal owner.
- Dedupe behaviour: if a document already exists in PandaDoc with the same deal_id stored on the HubSpot deal record, the agent must skip creation and post an alert rather than creating a duplicate. Check the 'pandadoc_document_id' custom HubSpot field before calling the create endpoint.
- Multi-product deals: if a single deal record contains multiple product line entries (HubSpot line items), the agent must iterate and produce one pricing row per line item in the PandaDoc token map. Confirm whether the PandaDoc template supports a line-item repeater block or requires a fixed number of rows.
- Google Docs is listed in the template as a connected tool. At runtime, the agent does not read from Google Docs. The Google Docs connection is used only during the discovery and pricing-mapping phase. Confirm with the process owner that all pricing rules have been exported before build begins.
Fires when the Proposal Assembly Agent marks the PandaDoc document as complete and passes the document URL and deal metadata to this agent. It posts a structured Slack message to the designated sales manager channel, containing the prospect name, deal value, resolved pricing summary, a PandaDoc preview link, and two inline interactive buttons: Approve and Request Changes. On Approve, the agent immediately sends the proposal to the prospect via Gmail using a pre-approved email template, then updates the HubSpot deal stage to 'Proposal Sent', logs a timestamped activity, and stores the PandaDoc document ID on the deal record. On Request Changes, the agent posts the manager's comment back to the rep in Slack and pauses the send flow. A timed reminder re-posts the approval request to Slack if no response is received within 4 hours. Build complexity is Moderate; the Slack interactive message handler and the two-branch approval logic are the primary complexity drivers.
// Input: payload from Proposal Assembly Agent completion event deal_id : string deal_name : string contact_firstname : string contact_email : string company_name : string resolved_total : number // USD pandadoc_doc_id : string pandadoc_doc_url : string // PandaDoc shareable preview link deal_owner_slack_id : string // Slack user ID of the assigned rep manager_slack_channel: string // confirmed channel ID for approval messages // On approval (manager clicks Approve in Slack) approval_action : 'approved' approved_by_slack_id : string approved_at : ISO8601 timestamp // On change request (manager clicks Request Changes) approval_action : 'changes_requested' manager_comment : string // free-text field from Slack modal // Output: proposal sent path gmail_message_id : string // returned by Gmail send API sent_at : ISO8601 timestamp hubspot_activity_id : string // logged engagement ID hubspot_deal_stage : 'Proposal Sent' pandadoc_doc_id_stored: string // written to custom HubSpot deal field // Output: change request path slack_notification_sent_to: string // deal_owner_slack_id flow_status : 'paused_awaiting_rep_edits'
- Slack bot configuration: the bot must be installed to the workspace with scopes chat:write, chat:write.public, and channels:read. Interactive components (approve/request-changes buttons) require the Slack app to have an Interactivity Request URL configured pointing to the automation platform's inbound webhook endpoint.
- Slack interactive message callback: the automation platform must expose a publicly reachable HTTPS endpoint to receive Slack action payloads. Validate the Slack signing secret on every inbound request before processing.
- Fallback reminder: if no Slack response (approve or request changes) is received within 4 hours of the initial post, the agent re-posts the approval message as a direct message to the manager's Slack user ID and logs a warning to the error table. Confirm the manager's Slack user ID during discovery.
- Gmail send: the sending Gmail account must be authenticated via OAuth 2.0. The approval and delivery agent uses a pre-approved email template stored in the automation platform; the template must be confirmed with the process owner before go-live. The Gmail API send scope (gmail.send) is required; do not use a broader scope.
- HubSpot deal stage update: the internal HubSpot pipeline stage ID for 'Proposal Sent' must be confirmed before wiring. Stage IDs are pipeline-specific and are not the same as the display label.
- PandaDoc document ID storage: a custom HubSpot deal property named 'pandadoc_document_id' (type: single-line text) must exist on the deal record before this agent runs. Confirm the property API name matches exactly.
- Error fallback: if the Gmail send call fails, the agent must not mark the HubSpot deal as 'Proposal Sent'. Log the failure, post a Slack alert to the deal owner, and leave the deal stage unchanged so the error is visible in the pipeline.
03End-to-end data flow
// ─────────────────────────────────────────────────────────────────
// TRIGGER: HubSpot webhook fires on deal stage change
// ─────────────────────────────────────────────────────────────────
HubSpot.webhook.dealstage -> 'Proposal Requested'
payload.deal_id : string
payload.dealstage : string // stage ID, not label
payload.contact_id : string
payload.company_id : string
payload.deal_name : string
payload.deal_value : number
payload.product_line : string // or line_items[] for multi-product
payload.discount_code : string?
payload.contact_firstname : string
payload.contact_lastname : string
payload.contact_email : string
payload.company_name : string
payload.proposal_expiry_days: number
payload.deal_owner_slack_id : string
// ─────────────────────────────────────────────────────────────────
// AGENT HANDOFF 1: Proposal Assembly Agent begins
// ─────────────────────────────────────────────────────────────────
// STEP 1: Field validation gate
validate([contact_email, company_name, deal_value, product_line])
ON missing_fields.length > 0:
-> log.error({ deal_id, missing_fields, timestamp })
-> Slack.postMessage(deal_owner_slack_id, 'Proposal halted: missing fields: {missing_fields}')
-> EXIT
// STEP 2: Dedupe check
HubSpot.getDealProperty(deal_id, 'pandadoc_document_id')
ON value != null:
-> log.warn({ deal_id, existing_doc_id: value })
-> Slack.postMessage(deal_owner_slack_id, 'Document already exists for this deal.')
-> EXIT
// STEP 3: Pricing resolution
PricingLookup.resolve({
product_line,
deal_value,
discount_code
})
-> resolved_unit_price : number
-> resolved_discount : number // percentage
-> resolved_total : number // USD
// STEP 4: PandaDoc document creation
PandaDoc.createDocument({
template_id : '<confirmed_template_uuid>',
name : deal_name + ' - Proposal',
recipients : [{ email: contact_email, role: 'Signer' }],
tokens : {
'[contact.firstname]' : contact_firstname,
'[contact.lastname]' : contact_lastname,
'[company.name]' : company_name,
'[deal.unit_price]' : resolved_unit_price,
'[deal.discount]' : resolved_discount,
'[deal.total]' : resolved_total,
'[deal.expiry_date]' : now() + proposal_expiry_days,
'[deal.scope]' : product_line
}
})
-> pandadoc_doc_id : string
-> pandadoc_doc_url : string
-> doc_status : 'draft'
// ───────────────���─────────────────────────────────────────────────
// AGENT HANDOFF 2: Approval and Delivery Agent begins
// Receives: deal_id, deal_name, contact_firstname, contact_email,
// company_name, resolved_total, pandadoc_doc_id,
// pandadoc_doc_url, deal_owner_slack_id
// ─────────────────────────────────────────────────────────────────
// STEP 5: Post Slack approval message
Slack.postInteractiveMessage({
channel : manager_slack_channel,
text : 'Proposal ready for review: {deal_name} | {company_name} | ${resolved_total}',
blocks : [preview_link(pandadoc_doc_url), button('Approve'), button('Request Changes')]
})
-> slack_message_ts : string // used to update message on response
// STEP 6a: Timed fallback (no response within 4 hours)
timer.wait(14400 seconds)
ON no_response:
-> Slack.postDirectMessage(manager_slack_id, 'Reminder: approval pending for {deal_name}')
-> log.warn({ deal_id, event: 'approval_reminder_sent', timestamp })
// STEP 6b: Manager responds — Approve branch
Slack.action.callback({ action_id: 'approve', approved_by_slack_id, approved_at })
Gmail.send({
to : contact_email,
subject : 'Your proposal from [YourCompany.com]: {deal_name}',
body : template('proposal_send_email', { contact_firstname, company_name, pandadoc_doc_url })
})
-> gmail_message_id : string
-> sent_at : ISO8601
HubSpot.updateDeal(deal_id, {
dealstage : '<Proposal Sent stage ID>',
pandadoc_document_id : pandadoc_doc_id
})
HubSpot.logActivity(deal_id, {
type : 'EMAIL',
subject : 'Proposal sent to {company_name}',
timestamp : sent_at
})
-> hubspot_activity_id : string
// STEP 6c: Manager responds — Request Changes branch
Slack.action.callback({ action_id: 'request_changes', manager_comment })
Slack.postDirectMessage(deal_owner_slack_id, {
text : 'Manager requested changes on {deal_name}: {manager_comment}',
link : pandadoc_doc_url
})
-> flow_status : 'paused_awaiting_rep_edits'
// ─────────────────────────────────────────────────────────────────
// END STATE (approve path)
// HubSpot deal: stage = 'Proposal Sent', pandadoc_document_id stored
// PandaDoc: document in 'sent' status, open tracking active
// Gmail: proposal email delivered to prospect
// Slack: approval message updated to 'Approved by {manager}' status
// ─────────────────────────────────────────────────────────────────04Recommended build stack
More documents for this process
Every document generated for Proposal Creation.