Back to Customer Satisfaction Surveying

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

Customer Satisfaction Surveying

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

This document gives the FullSpec build team everything required to implement, connect, and verify the Customer Satisfaction Surveying automation end to end. It covers the current-state step map with bottlenecks identified, full agent specifications with IO contracts, the complete data flow trace, and the recommended build stack. The owner side has no action items in this document. All build, configuration, and testing work described here is carried out by the FullSpec team.

01Process snapshot

Step
Name
Description
1
Identify Completed Tickets or Transactions
CX Coordinator manually filters or scrolls the HubSpot queue at end of day to spot tickets closed since the last review. Time cost: 20 min/cycle.
2
Check Customer Survey Eligibility
Coordinator cross-references a spreadsheet or scrolls sent emails to confirm the customer has not been surveyed in the past 30 days. Time cost: 15 min/cycle.
3
Personalise Survey Email
Coordinator copies a template into Gmail and manually edits customer name, ticket reference, and agent name. Each email is sent individually. Time cost: 30 min/cycle. BOTTLENECK.
4
Send Survey to Customer
Personalised email with embedded Typeform link is sent from Gmail. No open or click tracking is in place. Time cost: 10 min/cycle.
5
Log Survey Sent in Spreadsheet
Coordinator records customer name, date sent, ticket ID, and agent in a shared Google Sheet. Error-prone in bulk. Time cost: 10 min/cycle.
6
Monitor Typeform for Responses
Coordinator checks Typeform dashboard daily or when remembered. No automated alert fires on new submission, so low scores sit unseen for up to 24 hours. Time cost: 15 min/cycle. BOTTLENECK.
7
Copy Responses into Tracking Sheet
Each response is manually copied from Typeform into Google Sheets including score, comment, customer name, and date. Errors common in bulk sessions. Time cost: 25 min/cycle.
8
Flag Low Scores for Follow-Up
Coordinator reviews scores and manually marks any below the detractor threshold (CSAT under 3) for urgent follow-up. Time cost: 10 min/cycle.
9
Notify Support Manager of Detractors
Coordinator sends a Slack message or email to the support manager listing low-scoring customers. Often delayed or skipped on busy days. Time cost: 10 min/cycle.
10
Update CRM Contact Record
Support manager or coordinator manually updates the customer HubSpot contact record with the CSAT score and written comment. Time cost: 15 min/cycle.
11
Compile Weekly Satisfaction Report
Coordinator calculates averages, response rates, and trend notes from the spreadsheet and formats a summary every Friday. Time cost: 40 min/week. BOTTLENECK.
Time cost summary: Total manual time per cycle is 200 minutes (approximately 3 hours 20 minutes per batch run). Across a week running at approximately 120 surveys per month, the process consumes 5 hours of coordinator time every week, equivalent to around 250 hours per year at $38/hour. Steps 1 through 10 are replaced entirely by the two agents described in Section 02. Step 11 (weekly report compilation) is eliminated by the automated Google Sheets append and running averages built into the Response Processing Agent. The one retained human step is the support manager's direct follow-up call or message to a detractor, which requires empathy and context that cannot be automated.
Developer Handover PackPage 1 of 4
FS-DOC-04Technical

02Agent specifications

Survey Dispatch Agent

This agent watches the HubSpot pipeline for any ticket that transitions to Closed status. On firing, it queries the HubSpot contact record to confirm the customer has not received a survey in the past 30 days. If eligible, it assembles a personalised Gmail message using the customer name, ticket reference, and assigned agent name drawn from the ticket payload, sends it, and writes a survey-sent timestamp and ticket ID back to the HubSpot contact property. If the customer is ineligible, the run exits silently with a log entry. The agent operates without any manual trigger or review step. Estimated build time: 10 hours. Complexity: Moderate.

Trigger
HubSpot ticket pipeline stage changes to Closed
Tools
HubSpot, Gmail
Replaces steps
Steps 1, 2, 3, 4, 5
Estimated build
10 hours, Moderate complexity
// Input
hubspot_trigger.ticket_id        : string
hubspot_trigger.contact_id       : string
hubspot_trigger.pipeline_stage   : string  // must equal confirmed Closed stage label
hubspot_trigger.owner_name       : string  // assigned agent name for email personalisation
hubspot_contact.last_survey_sent : date | null

// Decision gate
IF (today - last_survey_sent) < 30 days -> EXIT (log: ineligible, skip)
IF last_survey_sent IS NULL -> PROCEED

// Output
gmail.send(
  to: hubspot_contact.email,
  subject: 'How did we do? Quick question for you',
  body: personalised_template(contact_first_name, ticket_id, owner_name, typeform_url)
)
hubspot.update_contact(
  contact_id: hubspot_trigger.contact_id,
  properties: {
    last_survey_sent: today_iso8601,
    last_survey_ticket_id: hubspot_trigger.ticket_id
  }
)
  • HubSpot pipeline stage name: the trigger condition must exactly match the Closed stage label as configured in the specific HubSpot account. Confirm the exact string during discovery before build begins, as it varies by account.
  • HubSpot contact property for last_survey_sent must be a date property. If it does not exist, create a custom contact property named hs_custom_last_survey_sent (date type) during setup.
  • HubSpot contact property for last_survey_ticket_id must be a single-line text property. Create hs_custom_last_survey_ticket_id if absent.
  • Gmail sending account: the OAuth connection must use the shared support Gmail account, not a personal account. Confirm the sending address with the operations lead before build.
  • Typeform survey URL must be a single static link embedded in the email template. Confirm the Typeform form ID and embed the pre-filled hidden fields (ticket_id, contact_id) in the URL query string so responses are attributable without manual matching.
  • Dedupe behaviour: if two tickets close for the same contact within 30 days, only the first triggers a send. The 30-day window is non-configurable at runtime; any change requires a workflow update.
  • Fallback: if the HubSpot contact record has no email address, the agent logs a warning row to the error log table and exits. No retry is attempted.
  • Confirm the detractor score threshold with the support manager before the Response Processing Agent build starts (not this agent, but a dependency that must be captured in discovery).
Response Processing Agent

This agent is triggered by an inbound Typeform webhook fired the moment a customer submits the survey. It parses the response payload to extract the CSAT score, written comment, submission timestamp, and the hidden fields carrying ticket_id and contact_id. It writes the score and comment to the corresponding HubSpot contact record, appends a structured row to the Google Sheets satisfaction tracking sheet, and evaluates the score against the agreed detractor threshold. If the score falls below the threshold, it posts a formatted Slack message to the support manager channel naming the customer, score, ticket reference, and a direct link to the HubSpot contact. All four actions run in sequence within a single workflow execution. Estimated build time: 14 hours. Complexity: Moderate.

Trigger
Typeform webhook: new survey response submitted
Tools
Typeform, HubSpot, Google Sheets, Slack
Replaces steps
Steps 6, 7, 8, 9, 10, 11
Estimated build
14 hours, Moderate complexity
// Input (Typeform webhook payload)
typeform_response.form_id              : string
typeform_response.submitted_at         : ISO8601 timestamp
typeform_response.answers[csat_score]  : integer (1-5)
typeform_response.answers[comment]     : string | null
typeform_response.hidden.ticket_id     : string
typeform_response.hidden.contact_id    : string

// Output: HubSpot write-back
hubspot.update_contact(
  contact_id: typeform_response.hidden.contact_id,
  properties: {
    last_csat_score: typeform_response.answers[csat_score],
    last_csat_comment: typeform_response.answers[comment],
    last_csat_date: typeform_response.submitted_at
  }
)

// Output: Google Sheets append
sheets.append_row(
  sheet_id: SATISFACTION_SHEET_ID,
  tab: 'Responses',
  values: [submitted_at, contact_id, ticket_id, csat_score, comment, detractor_flag]
)

// On detractor (csat_score < agreed_threshold)
slack.post_message(
  channel: '#support-manager-alerts',
  text: 'Detractor alert: [contact_name] scored [csat_score]/5 on ticket [ticket_id]. Comment: [comment]. View contact: [hubspot_contact_url]'
)
  • Typeform plan: webhooks require a paid Typeform plan (Basic or above). Confirm plan tier during discovery. If the account is on the free tier, switch to a polling approach (every 3 minutes) and document the added latency for the support manager.
  • Typeform hidden fields: the form must have hidden fields named ticket_id and contact_id pre-configured in the Typeform builder before the webhook is connected. Verify these exist or create them during setup.
  • Detractor threshold: agreed threshold is CSAT score under 3 (confirmed Jan 10 per lifecycle notes). This value must be stored as a configurable variable in the orchestration layer, not hardcoded, so the support manager can request a change without a full redeploy.
  • Google Sheets sheet ID and tab name: confirm the exact Google Sheets document ID and the tab named Responses with the operations lead. The sheet must have pre-built column headers matching the append order: Submitted At, Contact ID, Ticket ID, CSAT Score, Comment, Detractor Flag.
  • Slack channel name: confirm the exact channel name for detractor alerts with Tom Briggs (Support Manager) before build. The channel must exist and the Slack bot must be invited to it before the workflow is activated.
  • HubSpot custom contact properties for CSAT storage: last_csat_score (number type), last_csat_comment (multi-line text), last_csat_date (date) must be created in HubSpot if absent.
  • Fallback: if the HubSpot contact_id from the hidden field does not match any record, log an error row to the error table and post a low-priority Slack warning to the build alert channel. Do not silently discard the response.
  • Duplicate submissions: if the same form response ID is received twice (Typeform retry behaviour), check response ID against a deduplication store before processing. Skip and log if already processed.
Developer Handover PackPage 2 of 4
FS-DOC-04Technical

03End-to-end data flow

Full trigger-to-output data flow, Customer Satisfaction Surveying automation
// ───────────���─────────────────────────────────────────────────────
// TRIGGER: HubSpot ticket pipeline stage change
// ─────────────────────────────────────────────────────────────────
HubSpot.webhook(event: 'ticket.propertyChange')
  -> filter: pipeline_stage == '[CONFIRMED_CLOSED_STAGE_LABEL]'
  -> extract:
       ticket_id       : hs_ticket_id
       contact_id      : hs_associated_contact_id
       owner_name      : hs_ticket_assigned_agent_name
       contact_email   : hs_contact.email
       contact_fname   : hs_contact.firstname
       last_survey_sent: hs_contact.hs_custom_last_survey_sent

// ─────────────────────────────────────────────────────────────────
// AGENT HANDOFF 1: Survey Dispatch Agent begins
// ─────────────────────────────────────────────────────────────────
SurveyDispatchAgent.run(ticket_id, contact_id, owner_name,
                        contact_email, contact_fname, last_survey_sent)

  // Step 1: Eligibility check
  IF (TODAY - last_survey_sent) < 30 days:
    -> log_row(error_table, {ticket_id, contact_id, reason: 'ineligible_30d'})
    -> EXIT

  IF contact_email IS NULL:
    -> log_row(error_table, {ticket_id, contact_id, reason: 'no_email'})
    -> EXIT

  // Step 2: Assemble and send personalised email
  typeform_url = BASE_TYPEFORM_URL
               + '?ticket_id=' + ticket_id
               + '&contact_id=' + contact_id

  Gmail.send(
    from    : SUPPORT_GMAIL_ADDRESS,
    to      : contact_email,
    subject : 'How did we do? Quick question for you',
    body    : render_template(contact_fname, ticket_id, owner_name, typeform_url)
  )
  -> on_success: gmail_send_status = 'sent'
  -> on_failure: log_row(error_table, {ticket_id, reason: 'gmail_send_failed'}); EXIT

  // Step 3: Log send event to HubSpot
  HubSpot.update_contact(
    id: contact_id,
    hs_custom_last_survey_sent      : TODAY_ISO8601,
    hs_custom_last_survey_ticket_id : ticket_id
  )

// ────────────────────────────────────────────────────────────���────
// WAIT STATE: customer receives email, opens Typeform, submits
// ─────────────────────────────────────────────────────────────────

// ─────────────────────────────────────────────────────────────────
// TRIGGER: Typeform webhook on new response submission
// ─────────────────────────────────────────────────────────────────
Typeform.webhook(event: 'form_response')
  -> parse payload:
       typeform_response_id : response.token
       submitted_at         : response.submitted_at
       csat_score           : response.answers[field_id='csat'].number
       comment              : response.answers[field_id='comment'].text | null
       ticket_id            : response.hidden.ticket_id
       contact_id           : response.hidden.contact_id

// ──────────────────────────────────────────────────────���──────────
// AGENT HANDOFF 2: Response Processing Agent begins
// ─────────────────────────────────────────────────────────────────
ResponseProcessingAgent.run(typeform_response_id, submitted_at,
                            csat_score, comment, ticket_id, contact_id)

  // Step 1: Deduplicate
  IF dedup_store.contains(typeform_response_id):
    -> log_row(error_table, {typeform_response_id, reason: 'duplicate_submission'})
    -> EXIT
  ELSE:
    -> dedup_store.add(typeform_response_id)

  // Step 2: Verify contact exists in HubSpot
  hs_contact = HubSpot.get_contact(contact_id)
  IF hs_contact IS NULL:
    -> log_row(error_table, {contact_id, reason: 'contact_not_found'})
    -> Slack.post(BUILD_ALERT_CHANNEL, 'Warning: unmatched contact_id ' + contact_id)
    -> EXIT

  // Step 3: Write score to HubSpot contact
  HubSpot.update_contact(
    id: contact_id,
    last_csat_score   : csat_score,
    last_csat_comment : comment,
    last_csat_date    : submitted_at
  )

  // Step 4: Append row to Google Sheets
  detractor_flag = (csat_score < DETRACTOR_THRESHOLD) ? 'YES' : 'NO'
  GoogleSheets.append_row(
    sheet_id : SATISFACTION_SHEET_ID,
    tab      : 'Responses',
    values   : [submitted_at, contact_id, ticket_id,
                csat_score, comment, detractor_flag]
  )

  // Step 5: Conditional Slack alert (detractor path only)
  IF csat_score < DETRACTOR_THRESHOLD:
    hubspot_url = 'https://app.hubspot.com/contacts/' + hs_portal_id + '/contact/' + contact_id
    Slack.post_message(
      channel : '#support-manager-alerts',
      text    : ':rotating_light: Detractor | ' + hs_contact.firstname + ' '
                + hs_contact.lastname + ' scored ' + csat_score + '/5'
                + ' on ticket ' + ticket_id
                + ' | Comment: ' + (comment OR 'None')
                + ' | ' + hubspot_url
    )

// ─────────────────────────────────────────────────────────────────
// TERMINAL: Support Manager follows up with detractor (human step)
// ─────────────────────────────────────────────────────────────────
Developer Handover PackPage 3 of 4
FS-DOC-04Technical

04Recommended build stack

Orchestration layer
A workflow automation platform (not specified at this stage). Build one workflow per agent: 'Survey Dispatch Agent' and 'Response Processing Agent' as separate, independently testable workflows. Use a shared credential store within the platform for all API connections so credentials are not duplicated across workflows. Each workflow must be version-controlled with a descriptive change log entry on every update.
Webhook configuration
Survey Dispatch Agent: HubSpot native webhook or polling trigger on ticket pipeline stage property change, filtered to the confirmed Closed stage label. Response Processing Agent: Typeform webhook (paid plan required), pointing to the orchestration platform's inbound webhook URL. Secure the Typeform webhook endpoint with the platform's built-in HMAC signature verification using the Typeform webhook secret. Rotate the secret after initial configuration.
Templating approach
Survey email body: a plain-text and HTML dual-part template stored as a named template in the orchestration platform. Merge fields: {{contact_first_name}}, {{ticket_id}}, {{agent_name}}, {{survey_link}}. Slack detractor alert: a fixed-format message string assembled at runtime from response payload fields. No external templating engine is required.
Error logging
All error and skip events (ineligible contact, missing email, failed Gmail send, unmatched HubSpot contact, duplicate Typeform submission) are written as rows to a Supabase table named automation_error_log with columns: id (uuid), workflow_name (text), event_timestamp (timestamptz), ticket_id (text), contact_id (text), reason_code (text), raw_payload (jsonb). A Slack alert to the designated build monitoring channel fires for any error_code classified as critical (gmail_send_failed, contact_not_found). Non-critical skips (ineligible_30d, duplicate_submission) are logged only, no alert.
Testing approach
Sandbox-first. Both agents are built and validated against a HubSpot sandbox account and a staging Typeform form before any connection to production. Gmail sends in sandbox use an internal test address only. The Google Sheets target during testing is a dedicated staging sheet, not the live report. Slack alerts in testing route to a private #qa-testing channel. Full promotion to production occurs only after the FullSpec QA sign-off checklist is complete. See the Test and QA Plan (FS-DOC-06) for all test case IDs and pass criteria.
Estimated total build time
Survey Dispatch Agent: 10 hours. Response Processing Agent: 14 hours. End-to-end integration testing and QA: 4 hours. Total: 28 hours, matching the confirmed build_effort_hours in the process snapshot.
Three pre-build confirmations are required before either workflow is started: (1) The exact HubSpot pipeline stage label for Closed status, confirmed by the account admin. (2) The Typeform plan tier, confirmed by the operations lead, to determine webhook vs polling approach. (3) The detractor threshold score, confirmed by Tom Briggs (Support Manager). These are already noted as confirmed (CSAT under 3, Jan 10) in the lifecycle record, but must be verified against the live account before the build workflow is activated. Raise any discrepancy with the FullSpec team at support@gofullspec.com before proceeding.
All credential setup, OAuth authorisations, and API key storage are handled by the FullSpec team. The owner side is not required to configure any integration directly. For questions during or after build, contact the FullSpec team at support@gofullspec.com.
Developer Handover PackPage 4 of 4

More documents for this process

Every document generated for Customer Satisfaction Surveying.

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