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
Customer Testimonial Collection
[YourCompany.com] · Marketing Department · Prepared by FullSpec · [Today's Date]
This document is the complete technical reference for the FullSpec team building the Customer Testimonial Collection automation. It covers the current-state step map, agent specifications with IO contracts, the end-to-end data flow, and the recommended build stack. Everything needed to implement, test, and hand over the two-agent workflow is contained here. Where the owner side requires decisions, those are called out explicitly as confirmation items before build begins.
01Process snapshot
02Agent specifications
Monitors HubSpot for deals that transition to the Closed Won or Completed stage and manages the complete email outreach sequence on behalf of the Marketing Manager. The agent personalises each email using CRM contact and deal fields, embeds the Typeform submission link, creates a logged CRM note immediately after send, and schedules two timed follow-up reminders at day 7 and day 14 post-close. The sequence is cancelled automatically when a matching Typeform submission is detected, preventing over-contact. This agent replaces all manual email drafting, sending, logging, and reminder tasks. Estimated build: 12 hours. Complexity: Moderate.
// Input hubspot.deal.id : string // HubSpot deal ID hubspot.deal.stage : string // Must match confirmed stage label hubspot.contact.id : string // Associated primary contact hubspot.contact.email : string // Recipient address hubspot.contact.firstname: string // For email personalisation hubspot.contact.lastname : string // For email personalisation hubspot.deal.name : string // Project or deal name for body copy hubspot.deal.closedate : date // Used to anchor follow-up timing // Output gmail.message_id_initial : string // Message ID of initial outreach email gmail.message_id_followup1: string // Message ID of day-7 follow-up gmail.message_id_followup2: string // Message ID of day-14 follow-up (if needed) hubspot.note.id : string // CRM note recording outreach activity hubspot.note.body : string // 'Outreach sent [date]; template: testimonial_request_v1' typeform.form_url : string // Personalised Typeform link embedded in email // Cancellation condition typeform.submission_received: boolean // If true, cancel pending follow-up tasks immediately
- HubSpot pipeline stage names must be confirmed before build. The trigger fires on an exact string match. If the business uses multiple pipelines, each eligible stage label must be listed in the trigger configuration. Do not use deal stage ID alone as a fallback without confirming whether the account exposes stage IDs consistently via webhook.
- Gmail sending must use an OAuth 2.0 authenticated connection scoped to gmail.send only. The sending address should be the Marketing Manager's connected account, not a generic alias, to preserve deliverability and reply threading. Confirm the Google Workspace account holder grants OAuth consent before build begins.
- The Typeform link embedded in the email must be the specific form built for this process, not a generic feedback form. The form must include fields for testimonial text, customer name, customer role, and a permission/consent checkbox. Reusing an existing form without these fields will break the Submission Review and Library Agent's field mapping.
- Follow-up scheduling must use the deal close date as the anchor, not the send date of the initial email, to maintain consistent timing across all contacts. Day-7 and day-14 delays are relative to hubspot.deal.closedate.
- Dedupe behaviour: before sending the initial email, the agent must check whether a CRM note with the tag 'testimonial_outreach' already exists on the contact within the last 90 days. If found, skip send and log a 'duplicate suppressed' note. This prevents re-contacting customers across multiple deal cycles within the same quarter.
- The sequence cancellation check (Typeform submission received) must be evaluated before each scheduled follow-up fires, not only at the point of initial send.
- Confirm before build: exact HubSpot pipeline stage label(s), OAuth consent from sending Gmail account, Typeform form ID for the dedicated testimonial form, and whether any deal types should be excluded from outreach (for example, internal accounts or partner deals).
Receives Typeform submissions via webhook, constructs a formatted Slack approval card with the full testimonial content and two interactive buttons (Approve and Flag for Review), and routes the outcome accordingly. On approval, the agent writes a new entry to the Notion testimonial database and updates the HubSpot contact record with a 'Testimonial Collected' property and the approval date. On a Flag for Review response, the submission is held and a Slack thread reply is posted instructing the Marketing Manager to complete manual review before the record is written. This agent replaces all manual inbox monitoring, submission review, library entry, and team notification steps. Estimated build: 14 hours. Complexity: Moderate.
// Input (from Typeform webhook payload) typeform.form_id : string // Must match the dedicated testimonial form ID typeform.response_id : string // Used as idempotency key to prevent duplicate writes typeform.submitted_at : datetime // Submission timestamp typeform.answers.testimonial_text : string // Customer's submitted quote typeform.answers.customer_name : string // Customer's full name typeform.answers.customer_role : string // Customer's job title or role typeform.answers.permission_given : boolean // Consent checkbox — must be true to proceed typeform.hidden.hubspot_contact_id: string // Passed via Typeform hidden field from email link // Output (on Approve path) notion.page_id : string // ID of the new Notion testimonial library entry notion.page.customer_name : string notion.page.customer_role : string notion.page.testimonial_text : string notion.page.approval_date : date notion.page.hubspot_contact_id: string // Cross-reference field hubspot.contact.property.testimonial_collected : boolean // Set to true hubspot.contact.property.testimonial_approval_date : date slack.notification_message : string // 'New testimonial saved to library: [customer_name]' // On approval (Slack interactive action) slack.action.value : string // 'approve' or 'flag_for_review' slack.action.user_id : string // Slack user ID of the approving manager slack.action.action_ts : string // Timestamp of button click // On Flag for Review path slack.thread_reply : string // 'Submission held — please review and approve manually before library write.' notion.write_blocked : boolean // No Notion page created until manual resolution
- The Slack bot requires the chat:write, channels:read, and interactive_messages (interactivity) permission scopes enabled in the Slack app configuration. Organisations with strict Slack governance may require IT approval to enable interactive message actions on a bot. Confirm this access is available before build begins.
- The Typeform webhook must be secured with a signature verification header (Typeform-Signature) validated on receipt. Do not process payloads without signature verification to prevent spoofed submissions from writing to Notion.
- The permission_given field (consent checkbox) must be validated as true before any downstream action. If the field is false or absent, the workflow must halt, log a 'consent not given' error to the Supabase error table, and post a Slack alert to the marketing channel.
- Use typeform.response_id as an idempotency key. Before writing to Notion, query the database for an existing page with a matching response_id. If found, skip the write and log a 'duplicate suppressed' event. This prevents double entries from webhook retries.
- The Notion database schema must include the following properties before build: Customer Name (title), Customer Role (rich text), Testimonial Text (rich text), Approval Date (date), HubSpot Contact ID (rich text), Response ID (rich text, used for deduplication), and Status (select: Approved, Flagged, Pending).
- The hubspot_contact_id must be passed as a Typeform hidden field appended to the form URL at the point of link generation in the Outreach Sequence Agent. Confirm the Typeform plan tier supports hidden fields (all paid plans support this; the free tier does not).
- HubSpot contact property 'testimonial_collected' (boolean) and 'testimonial_approval_date' (date) must be created as custom contact properties in the HubSpot account before build. Confirm these do not already exist under a different label.
- Confirm before build: Slack workspace admin access to install and configure the bot app, Typeform plan tier (must support hidden fields and webhooks), Notion database ID for the testimonial library, and HubSpot custom property names as they will appear in the API.
03End-to-end data flow
// ============================================================
// CUSTOMER TESTIMONIAL COLLECTION — END-TO-END DATA FLOW
// Volume: ~30 cycles/month | Agents: 2 | Human steps retained: 1
// ============================================================
// TRIGGER
// Source: HubSpot deal webhook
HubSpot.deal.stage_changed
-> deal.id : string // e.g. '12345678'
-> deal.stage : string // Must equal confirmed 'Closed Won' or 'Completed' label
-> deal.name : string // e.g. 'Acme Corp — Website Redesign Q2'
-> deal.closedate : date // Anchor for follow-up timing
-> contact.id : string // Primary associated contact ID
-> contact.email : string // Recipient address
-> contact.firstname : string
-> contact.lastname : string
// ============================================================
// AGENT 1: Outreach Sequence Agent
// ============================================================
// STEP 1 — Dedupe check
HubSpot.notes.query(contact.id, tag='testimonial_outreach', days=90)
-> IF note_found == true : halt; log 'duplicate suppressed' to error_log
-> IF note_found == false : continue
// STEP 2 — Send initial testimonial request email
Gmail.send(
to : contact.email,
from : manager_oauth_address, // OAuth-authenticated Marketing Manager account
subject : 'A quick favour, {contact.firstname}',
body : template_testimonial_request_v1, // Populated with deal.name, contact.firstname
typeform_link: 'https://form.typeform.com/to/{FORM_ID}#hubspot_contact_id={contact.id}'
)
-> gmail.message_id_initial : string // Stored for thread reference
// STEP 3 — Log outreach note in HubSpot
HubSpot.notes.create(
contact_id : contact.id,
body : 'Testimonial outreach sent {send_date}; template: testimonial_request_v1',
tag : 'testimonial_outreach'
)
-> hubspot.note.id : string
// STEP 4 — Schedule follow-up 1 (day 7 from deal.closedate)
// Before send: check typeform.submission_received for contact.id
IF typeform.submission_received == false:
Gmail.send(
to : contact.email,
subject : 'Just checking in, {contact.firstname}',
body : template_followup_1_v1
)
-> gmail.message_id_followup1 : string
ELSE: cancel; log 'follow-up 1 suppressed — submission received'
// STEP 5 — Schedule follow-up 2 (day 14 from deal.closedate)
// Before send: check typeform.submission_received for contact.id
IF typeform.submission_received == false:
Gmail.send(
to : contact.email,
subject : 'Last chance to share your experience',
body : template_followup_2_v1
)
-> gmail.message_id_followup2 : string
ELSE: cancel; log 'follow-up 2 suppressed — submission received'
// ============================================================
// HANDOFF: Outreach Sequence Agent -> Submission Review and Library Agent
// Trigger: Typeform webhook fires on new submission
// ============================================================
Typeform.webhook.receive(
form_id : '{FORM_ID}', // Validated against expected form ID
signature : Typeform-Signature header // Must pass HMAC-SHA256 verification
)
-> typeform.response_id : string
-> typeform.submitted_at : datetime
-> typeform.answers.testimonial_text : string
-> typeform.answers.customer_name : string
-> typeform.answers.customer_role : string
-> typeform.answers.permission_given : boolean
-> typeform.hidden.hubspot_contact_id: string
// ============================================================
// AGENT 2: Submission Review and Library Agent
// ============================================================
// STEP 6 — Consent and dedupe validation
IF typeform.answers.permission_given == false:
halt; log 'consent_not_given' to error_log; post Slack alert to #marketing
IF Notion.query(response_id == typeform.response_id).exists:
halt; log 'duplicate_suppressed' to error_log
// STEP 7 — Post Slack approval card
Slack.post_message(
channel : '#marketing-approvals',
blocks : [
header : 'New testimonial submission — action required',
customer : '{typeform.answers.customer_name}, {typeform.answers.customer_role}',
quote : '{typeform.answers.testimonial_text}',
button_1 : { text: 'Approve', action_id: 'approve_testimonial', value: 'approve' },
button_2 : { text: 'Flag for Review', action_id: 'flag_testimonial', value: 'flag_for_review' }
]
)
-> slack.message_ts : string // Used to update card after action
// STEP 8 — Wait for Slack interactive action callback
Slack.interaction.receive(
action_id : 'approve_testimonial' | 'flag_testimonial',
user_id : string,
action_ts : string
)
// BRANCH A — Approved
IF slack.action.value == 'approve':
// STEP 9 — Write to Notion testimonial library
Notion.pages.create(
database_id : '{NOTION_DB_ID}',
customer_name : typeform.answers.customer_name, // Title property
customer_role : typeform.answers.customer_role,
testimonial_text : typeform.answers.testimonial_text,
approval_date : now(),
hubspot_contact_id: typeform.hidden.hubspot_contact_id,
response_id : typeform.response_id, // Dedup field
status : 'Approved'
)
-> notion.page_id : string
// STEP 10 — Update HubSpot contact record
HubSpot.contacts.update(
contact_id : typeform.hidden.hubspot_contact_id,
testimonial_collected : true,
testimonial_approval_date : now()
)
// STEP 11 — Post Slack confirmation to #marketing
Slack.post_message(
channel : '#marketing',
text : 'New testimonial saved to library: {typeform.answers.customer_name} — {typeform.answers.customer_role}'
)
Slack.update_message(ts: slack.message_ts, text: 'Approved by {slack.action.user_id}')
// BRANCH B — Flagged for Review
IF slack.action.value == 'flag_for_review':
Notion.pages.create(... status: 'Flagged') // Held record, not published
Slack.post_thread_reply(
ts : slack.message_ts,
text : 'Submission held — please review and approve manually before library write.'
)
// ============================================================
// END OF FLOW
// ============================================================04Recommended build stack
More documents for this process
Every document generated for Customer Testimonial Collection.