Back to Customer Feedback Loop

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 Feedback Loop

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

This document gives the FullSpec build team everything needed to implement, test, and hand over the Customer Feedback Loop automation. It covers the current-state process map with bottleneck flags, full specifications for each of the three agents, an end-to-end data flow trace, and the recommended build stack. The FullSpec team owns all build and integration work described here. Your team's responsibility is limited to supplying confirmed API credentials, approving sentiment score thresholds, and reviewing escalated negative feedback alerts once the system is live.

01Process snapshot

Step
Name
Description
1
Check Each Feedback Channel
Operations Manager manually checks email inboxes, survey dashboards, and review platforms for new responses. Done daily or every few days depending on workload. Time cost: 25 min/cycle.
2
Copy Feedback Into Spreadsheet
Each response is copied by hand into a shared Google Sheet including date, source, customer name, and raw comment. Formatting is inconsistent across entries. Time cost: 20 min/cycle.
3
Read and Assess Sentiment
BOTTLENECK. Team member reads each response and makes an unstructured judgement call on sentiment. No scoring system exists, so the same comment may be categorised differently by different people. Time cost: 20 min/cycle.
4
Flag Urgent or Negative Feedback
BOTTLENECK. Responses judged as urgent are highlighted in the spreadsheet or forwarded via email to a manager. Routing is slow when the responsible person is unclear. Time cost: 15 min/cycle.
5
Update CRM Contact Record
If the feedback is tied to a known customer, the team member finds the HubSpot record and adds a note manually. Many records are never updated due to time pressure. Time cost: 20 min/cycle.
6
Draft and Send Acknowledgement Email
A reply is written and sent via Gmail confirming receipt. Tone and content vary depending on who sends it and how busy they are. Time cost: 15 min/cycle.
7
Log Themes in Notion
Once a week the manager reviews the spreadsheet and writes a short summary of recurring themes into a Notion page. This review rarely happens on time. Time cost: 30 min/cycle.
8
Share Weekly Summary With Team
A summary of feedback themes is copied into a Slack message or email and shared with staff. Often brief and lacking actionable detail. Time cost: 15 min/cycle.
Time cost summary: Total manual time per processing cycle is 160 minutes (2 hours 40 minutes). At approximately 120 responses per month and batched processing, this equates to roughly 6 hours of manual effort per week and 300 hours per year. The automation replaces Steps 1 through 7 in full. Step 8 (Slack summary sharing) is superseded by the automated Notion dashboard and real-time Slack alerts. The only step retained by a human is the review and decision on escalated negative feedback surfaced by the Slack alert in the Sentiment and Routing Agent.
Developer Handover PackPage 1 of 4
FS-DOC-04Technical

02Agent specifications

Feedback Intake Agent

Monitors two connected input channels for new feedback: a Typeform webhook and a Gmail inbox filter for feedback-tagged messages. On each trigger event the agent extracts structured fields including customer identity, source channel, submission timestamp, and raw response text, then writes a normalised record to the central Google Sheet. This agent is the entry point for the entire workflow and its output quality directly determines the reliability of all downstream scoring and routing.

Trigger
New Typeform response submitted via webhook OR new email matching feedback label/filter arrives in the connected Gmail inbox.
Tools
Typeform, Gmail, Google Sheets
Replaces steps
Step 1 (Check Each Feedback Channel) and Step 2 (Copy Feedback Into Spreadsheet)
Estimated build
9 hours / Moderate
// Input
Typeform: form_id, response_id, submitted_at, answers[]
  answers[]: field_id, field_type, field_ref, value (text|choice|rating)
Gmail: message_id, thread_id, from_email, subject, body_plain, received_at

// Output (to Google Sheets row)
record_id          : uuid v4 generated at intake
source             : 'typeform' | 'gmail'
submitted_at       : ISO 8601 timestamp (UTC)
customer_email     : string (extracted from Typeform answer or Gmail from_email)
customer_name      : string | null
raw_text           : string (full response body or email body_plain)
intake_status      : 'pending_scoring'
gmail_message_id   : string | null (populated for Gmail source only)
typeform_response_id: string | null (populated for Typeform source only)
  • Typeform plan must be at least the Basic tier to expose webhook events. Confirm plan level before build starts. The webhook endpoint must be registered under Typeform's Connect > Webhooks panel with the automation platform's inbound URL.
  • Gmail connection requires OAuth 2.0 with scopes gmail.readonly and gmail.modify. A dedicated Gmail label named 'feedback-intake' must be created and all relevant filters applied before the agent is activated. The agent polls or listens for this label only, not the full inbox.
  • Google Sheets target must have a named sheet tab 'feedback_log' with columns pre-created in this exact order: record_id, source, submitted_at, customer_email, customer_name, raw_text, sentiment_score, theme_tag, sentiment_label, hubspot_contact_id, hubspot_update_status, ack_email_sent, notion_appended, intake_status.
  • Dedupe: check record_id and typeform_response_id or gmail_message_id against existing rows before appending. If a duplicate is found, skip and log to error table with reason 'duplicate_intake'.
  • Fallback: if raw_text is empty or under 5 characters after trim, set intake_status to 'invalid_response' and halt the record. Do not pass it to the Sentiment and Routing Agent.
  • Confirm before build: the exact Typeform form IDs to monitor, the Gmail account address and label naming convention, and whether the Google Sheet is owned by a service account or a user OAuth token.
Sentiment and Routing Agent

Reads each pending record from the Google Sheet written by the Intake Agent, sends the raw_text to an AI language model for sentiment scoring and theme classification, updates the Sheet row with results, attempts a HubSpot contact match on customer_email, appends a feedback note to the matched contact record, and fires a Slack alert for any score of 4 or below. This agent contains the primary business logic of the workflow and must handle both matched and unmatched HubSpot contacts gracefully.

Trigger
New row appended to the 'feedback_log' sheet with intake_status equal to 'pending_scoring'.
Tools
Google Sheets, HubSpot, Slack
Replaces steps
Step 3 (Read and Assess Sentiment), Step 4 (Flag Urgent or Negative Feedback), Step 5 (Update CRM Contact Record)
Estimated build
11 hours / Complex
// Input (from Google Sheets row)
record_id          : uuid
raw_text           : string
customer_email     : string | null
customer_name      : string | null
source             : 'typeform' | 'gmail'
submitted_at       : ISO 8601 timestamp

// AI scoring payload
prompt_input       : raw_text
model_response: {
  sentiment_score  : integer 1-10
  sentiment_label  : 'positive' | 'neutral' | 'negative'
  theme_tag        : 'product_quality' | 'delivery' | 'support' | 'pricing' | 'other'
}

// HubSpot lookup
search_by          : customer_email
match_result       : { contact_id: string } | null

// Output (Sheet row updated)
sentiment_score    : integer 1-10
sentiment_label    : string
theme_tag          : string
hubspot_contact_id : string | 'no_match'
hubspot_update_status: 'updated' | 'no_match' | 'error'
intake_status      : 'scored'

// Slack alert (when sentiment_score <= 4)
channel            : #feedback-alerts
payload: {
  customer_name    : string | 'Unknown'
  customer_email   : string
  sentiment_score  : integer
  theme_tag        : string
  raw_text_preview : first 280 characters of raw_text
  submitted_at     : formatted local time
  hubspot_link     : url | 'No CRM match'
}
  • The AI scoring call must use a system prompt that anchors the 1-to-10 scale: 1 to 3 is strongly negative with explicit dissatisfaction, 4 to 6 is mixed or neutral, 7 to 10 is clearly positive. The prompt must instruct the model to return only valid JSON with keys sentiment_score, sentiment_label, and theme_tag. No prose.
  • Threshold for Slack alert is sentiment_score 4 or below. This value must be stored as a workflow variable named SENTIMENT_ALERT_THRESHOLD so it can be updated without touching node logic. Confirm the agreed threshold value with the process owner before go-live.
  • HubSpot API: use the Contacts Search endpoint (POST /crm/v3/objects/contacts/search) filtering by the email property. If zero results are returned, set hubspot_contact_id to 'no_match' and hubspot_update_status to 'no_match'. Do not create a new contact automatically.
  • HubSpot note creation: use the Engagements API (POST /crm/v3/objects/notes) with associations to the matched contact_id. The note body must include sentiment_score, sentiment_label, theme_tag, source, and submitted_at. A custom HubSpot contact property named 'latest_feedback_score' (type: number) must be created in the HubSpot portal before build and updated on each match.
  • HubSpot tier must support private app tokens with scopes crm.objects.contacts.read, crm.objects.contacts.write, crm.objects.notes.write, and crm.schemas.contacts.write. Confirm the portal plan supports private apps (Starter or above).
  • Slack connection requires a Slack App installed to the workspace with the chat:write scope and the target channel ID for #feedback-alerts. Confirm channel name and workspace before build.
  • Fallback: if the AI scoring call fails or returns malformed JSON after two retries, set intake_status to 'scoring_error', log to the error table, and send a fallback Slack alert to #automation-errors with the record_id and raw error message.
  • Short or ambiguous responses (under 20 words) should be flagged in the note body as 'low_confidence_score: true' so the process owner can review scoring accuracy during the first four weeks of live operation.
Response and Reporting Agent

Reads each row where intake_status is 'scored', selects the correct Gmail acknowledgement template based on sentiment_label, personalises and sends the email to customer_email, then appends the classified record to the Notion feedback themes dashboard. This agent closes the customer-facing loop and keeps the reporting layer current without any manual effort. It must not send duplicate acknowledgement emails if triggered more than once for the same record_id.

Trigger
Google Sheet row updated with intake_status equal to 'scored' by the Sentiment and Routing Agent.
Tools
Gmail, Notion
Replaces steps
Step 6 (Draft and Send Acknowledgement Email) and Step 7 (Log Themes in Notion)
Estimated build
8 hours / Moderate
// Input (from Google Sheets row)
record_id          : uuid
customer_email     : string
customer_name      : string | 'there'  // fallback salutation
sentiment_label    : 'positive' | 'neutral' | 'negative'
sentiment_score    : integer 1-10
theme_tag          : string
submitted_at       : ISO 8601 timestamp

// Template selection logic
if sentiment_label == 'positive'  -> template_id: 'ack_positive'
if sentiment_label == 'neutral'   -> template_id: 'ack_neutral'
if sentiment_label == 'negative'  -> template_id: 'ack_negative'

// Gmail send payload
to                 : customer_email
from               : connected Gmail address
subject            : resolved from template (includes customer_name)
body_html          : template rendered with { customer_name, theme_tag, submitted_at }

// Notion append payload
database_id        : <confirmed Notion DB id>
properties: {
  Name             : customer_name | customer_email
  Email            : customer_email
  Score            : sentiment_score
  Sentiment        : sentiment_label
  Theme            : theme_tag
  Source           : source
  Submitted        : submitted_at
  Record_ID        : record_id
}

// Output (Sheet row updated)
ack_email_sent     : true | false
notion_appended    : true | false
intake_status      : 'complete'
  • Gmail send requires OAuth 2.0 scope gmail.send on the same connected account used by the Intake Agent. The From address shown to customers must be agreed with the process owner before build.
  • Three email templates must be authored and stored in the automation platform before this agent is activated: ack_positive, ack_neutral, and ack_negative. Template content must be approved by the process owner. Each template must support variable substitution for customer_name, theme_tag, and submitted_at.
  • Dedupe guard: before sending, check ack_email_sent on the Sheet row. If already true, skip send and log reason 'duplicate_send_prevented' to the error table.
  • Notion integration requires a Notion internal integration token with insert access to the target database. The database must have the following property types pre-created: Name (title), Email (email), Score (number), Sentiment (select with options positive, neutral, negative), Theme (select), Source (select), Submitted (date), Record_ID (rich text). Confirm the Notion database ID before build.
  • If customer_email is null or malformed, skip the Gmail send step, set ack_email_sent to false, and still proceed with the Notion append.
  • If the Notion append fails after two retries, set notion_appended to false, update intake_status to 'complete_notion_error', and log the failure to the error table with record_id.
  • Confirm before build: whether the process owner wants the negative acknowledgement email sent immediately or held pending the Slack alert review. The default build sends immediately for all three sentiment categories.
Developer Handover PackPage 2 of 4
FS-DOC-04Technical

03End-to-end data flow

Customer Feedback Loop: full trigger-to-output data flow with agent handoff points and field names
// ───────────────────────────────────────���─────────────────────────
// TRIGGER
// ─────────────────────────────────────────────────────────────────
SOURCE: Typeform webhook (form_id, response_id, submitted_at, answers[])
     OR Gmail inbox filter (message_id, thread_id, from_email, subject,
                            body_plain, received_at)

// ─────────────────────────────────────────────────────────────────
// AGENT 1: Feedback Intake Agent
// ─────────────────────────────────────────────────────────────────
EXTRACT:
  customer_email    <- answers[email_field].value | from_email
  customer_name     <- answers[name_field].value | null
  raw_text          <- answers[comment_field].value | body_plain
  source            <- 'typeform' | 'gmail'
  submitted_at      <- submitted_at | received_at (normalised to UTC ISO 8601)

GENERATE:
  record_id         <- uuid_v4()
  intake_status     <- 'pending_scoring'

DEDUPE CHECK:
  if record_id or source_message_id exists in feedback_log -> HALT, log error
  if len(raw_text.trim()) < 5                              -> intake_status = 'invalid_response', HALT

WRITE -> Google Sheets [feedback_log]:
  record_id, source, submitted_at, customer_email,
  customer_name, raw_text, intake_status='pending_scoring'

// ── HANDOFF: Intake Agent -> Sentiment and Routing Agent ──────────
// Trigger condition: new row with intake_status = 'pending_scoring'

// ─────────────────────────────────────────────────────────────────
// AGENT 2: Sentiment and Routing Agent
// ─────────────────────────────────────────────────────────────────
READ <- Google Sheets [feedback_log] row where intake_status = 'pending_scoring'
  fields: record_id, raw_text, customer_email, customer_name, submitted_at

AI SCORING CALL:
  input:  raw_text
  output: sentiment_score  (integer 1-10)
          sentiment_label  ('positive' | 'neutral' | 'negative')
          theme_tag        ('product_quality'|'delivery'|'support'|'pricing'|'other')
  on_fail (after 2 retries): intake_status='scoring_error', log error, Slack #automation-errors

HUBSPOT LOOKUP:
  POST /crm/v3/objects/contacts/search
  filter: email = customer_email
  on_match:
    hubspot_contact_id     <- contact.id
    POST /crm/v3/objects/notes (body: sentiment_score, sentiment_label,
                               theme_tag, source, submitted_at)
    PATCH /crm/v3/objects/contacts/{id}
      properties.latest_feedback_score <- sentiment_score
    hubspot_update_status  <- 'updated'
  on_no_match:
    hubspot_contact_id     <- 'no_match'
    hubspot_update_status  <- 'no_match'

ROUTING DECISION:
  if sentiment_score <= SENTIMENT_ALERT_THRESHOLD (default: 4):
    POST Slack channel #feedback-alerts
      payload: customer_name, customer_email, sentiment_score,
               theme_tag, raw_text[0:280], submitted_at, hubspot_link

UPDATE -> Google Sheets [feedback_log] row:
  sentiment_score, sentiment_label, theme_tag,
  hubspot_contact_id, hubspot_update_status,
  intake_status <- 'scored'

// ── HANDOFF: Sentiment and Routing Agent -> Response and Reporting Agent ──
// Trigger condition: row updated with intake_status = 'scored'

// ────────────────────────────────────────────────────────��────────
// AGENT 3: Response and Reporting Agent
// ─────────────────────────────────────────────────────────────────
READ <- Google Sheets [feedback_log] row where intake_status = 'scored'
  fields: record_id, customer_email, customer_name, sentiment_label,
          sentiment_score, theme_tag, submitted_at, ack_email_sent

DEDUPE CHECK:
  if ack_email_sent = true -> SKIP send, log 'duplicate_send_prevented'

TEMPLATE SELECTION:
  sentiment_label = 'positive' -> template_id = 'ack_positive'
  sentiment_label = 'neutral'  -> template_id = 'ack_neutral'
  sentiment_label = 'negative' -> template_id = 'ack_negative'

GMAIL SEND:
  if customer_email is valid:
    POST Gmail send
      to: customer_email
      subject: resolved from template
      body_html: render(template_id, {customer_name, theme_tag, submitted_at})
    ack_email_sent <- true
  else:
    ack_email_sent <- false

NOTION APPEND:
  POST Notion database (database_id: <confirmed>)
    properties: Name, Email, Score, Sentiment, Theme,
                Source, Submitted, Record_ID
  on_success: notion_appended <- true
  on_fail (after 2 retries): notion_appended <- false,
                             intake_status <- 'complete_notion_error',
                             log to error table

UPDATE -> Google Sheets [feedback_log] row:
  ack_email_sent, notion_appended,
  intake_status <- 'complete'

// ─────────────────────────────────────────────────────────────────
// END STATE: record fully processed
// Google Sheets: complete row with all fields populated
// HubSpot: contact note added and latest_feedback_score updated (if matched)
// Slack: alert posted (if sentiment_score <= 4)
// Gmail: acknowledgement sent to customer
// Notion: entry appended to themes dashboard
// ─────────────────────────────────────────────────────────────────
Developer Handover PackPage 3 of 4
FS-DOC-04Technical

04Recommended build stack

Orchestration layer
A workflow automation platform configured with one workflow per agent (three workflows total): 'feedback-intake', 'sentiment-routing', and 'response-reporting'. All tool credentials are stored in a shared credential store within the platform so rotating a token affects all workflows that reference it simultaneously. Workflows are named with the prefix 'cfl-' (Customer Feedback Loop) for namespace clarity in shared workspaces.
Webhook configuration
The Feedback Intake Agent exposes one inbound webhook URL registered in Typeform under Connect > Webhooks. The webhook must include a secret header (X-Typeform-Signature) verified on receipt. For Gmail intake, a polling interval of 2 minutes is recommended using the Gmail label filter rather than a push subscription, to avoid OAuth re-authorisation complexity. Both intake paths converge to the same normalisation logic before writing to Google Sheets.
Templating approach
The three acknowledgement email templates (ack_positive, ack_neutral, ack_negative) are stored as HTML template strings within the orchestration layer's credential or environment variable store, not hard-coded in node logic. Variable substitution uses double-brace syntax: {{customer_name}}, {{theme_tag}}, {{submitted_at}}. Templates must be reviewed and signed off by the process owner before the Response and Reporting Agent is activated in production.
Error logging
All error events (duplicate intake, invalid response, scoring failure, HubSpot error, Notion failure, duplicate send prevented) are written to a Supabase table named 'cfl_error_log' with columns: error_id (uuid), record_id (uuid), error_type (varchar), error_detail (text), occurred_at (timestamptz), resolved (boolean, default false). A Slack alert to #automation-errors is triggered for any error_type containing 'scoring_error' or 'notion_error'. The process owner reviews the error log weekly during the first four weeks of live operation.
Testing approach
All three agents are built and tested in a sandbox environment first. Typeform sandbox submissions use the Typeform preview mode. A dedicated test Gmail label 'feedback-intake-test' is used during build and switched to 'feedback-intake' at go-live. A HubSpot sandbox portal is used for contact lookup and note creation tests. Notion testing targets a duplicate database named 'Feedback Themes (TEST)'. Sandbox credentials are stored separately from production credentials in the platform's credential store and swapped at launch.
SENTIMENT_ALERT_THRESHOLD variable
Stored as a workflow-level environment variable with a default value of 4. Must not be hard-coded in node logic. The process owner can request a threshold change via support@gofullspec.com and FullSpec updates the variable without a rebuild.
Estimated total build time
Feedback Intake Agent: 9 hours. Sentiment and Routing Agent: 11 hours. Response and Reporting Agent: 8 hours. End-to-end integration testing, threshold tuning, and error log setup: 8 hours (included in the 28-hour total confirmed in the project snapshot). Total: 28 hours across a 3 to 4 week delivery window aligned with the confirmed delivery schedule.
Before build starts, the FullSpec team requires the following to be confirmed and supplied: (1) Typeform form IDs and plan tier confirmation, (2) Gmail address for intake and send, plus label naming, (3) Google Sheets document ID and service account or OAuth token decision, (4) HubSpot portal ID and private app token with confirmed scopes, (5) Slack workspace, Bot token, and #feedback-alerts channel ID, (6) Notion integration token and target database ID, (7) agreed sentiment score threshold and email template content approved by the process owner. Contact support@gofullspec.com to submit these details.
Developer Handover PackPage 4 of 4

More documents for this process

Every document generated for Customer Feedback Loop.

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