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
GDPR / Data Privacy Request Handling
[YourCompany.com] · Legal Department · Prepared by FullSpec · [Today's Date]
This document gives the FullSpec build team everything needed to configure, connect, and test the GDPR and data privacy request handling automation from first trigger to closed audit log. It covers the full current-state step map with time costs, both agent specifications with IO contracts and implementation constraints, the end-to-end data flow as a traceable code block, and the recommended build stack with total effort estimate. The process owner's role is limited to confirming API access, approving draft responses in Notion, and accepting the handover once QA is complete. FullSpec builds, tests, and monitors all automation logic.
01Process snapshot
02Agent specifications
Receives a structured Typeform submission the moment a data subject submits a privacy request. The agent classifies the request type (Subject Access Request, erasure, or rectification), creates a structured case record in Notion with the submission timestamp and calculated 30-day deadline, and dispatches a templated identity verification email via Gmail, all without any manual input. This agent is the entry point for every case and is responsible for the completeness and accuracy of the initial Notion record, which becomes the single source of truth for the entire workflow.
// Input (from Typeform webhook payload) form_response.answers.requester_full_name // string form_response.answers.requester_email // string, validated email format form_response.answers.request_type // enum: SAR | erasure | rectification form_response.answers.additional_context // string, optional free text form_response.submitted_at // ISO 8601 timestamp // Output (written to Notion case record) notion.case_id // auto-generated Notion page ID notion.requester_name // mapped from requester_full_name notion.requester_email // mapped from requester_email notion.request_type // mapped from request_type notion.submission_date // mapped from submitted_at notion.deadline_date // submitted_at + 30 calendar days notion.identity_status // default: 'Pending' notion.case_status // default: 'Open' // Output (Gmail verification email) gmail.to // requester_email gmail.subject // 'Your privacy request has been received - action required' gmail.body // templated: includes request_type, deadline_date, ID requirements gmail.sent_timestamp // recorded back to Notion case record
- Typeform must be on a paid plan (Basic or above) to enable webhook delivery. Confirm the workspace admin can create a new form and activate webhook publishing before build starts.
- The Notion integration token must have 'Insert content', 'Read content', and 'Update content' permissions on the target database. The database must include the following properties at minimum: Case ID (title), Requester Name (text), Requester Email (email), Request Type (select), Submission Date (date), Deadline Date (date), Identity Status (select: Pending, Confirmed, Failed), Case Status (select: Open, In Progress, Pending Review, Closed), Verification Email Sent (checkbox), Sent Timestamp (date).
- Gmail authentication must use OAuth 2.0 with the gmail.send scope. A dedicated service account or shared mailbox (for example privacy@[YourCompany.com]) is strongly recommended so the sender address is consistent and auditable.
- The verification email template must be finalised and approved by the Legal Adviser before build. The template must reference the specific request type dynamically so that SAR, erasure, and rectification requests receive appropriately worded emails.
- Deadline calculation must account for calendar days, not business days, in line with GDPR Article 12. The platform must compute submission_date + 30 days precisely and store the result in Notion before any further steps run.
- Dedupe behaviour: if a Typeform submission is received with a requester_email that already has an Open or In Progress case record in Notion (created within the last 60 days), the agent must flag the duplicate to the Operations Manager via Slack rather than creating a second record. It must not suppress the submission silently.
- Fallback: if the Notion write fails, the agent must retain the Typeform payload and retry up to three times with exponential backoff (30 s, 2 min, 5 min). If all retries fail, an alert email must be sent to the process owner and the error logged to the error table.
Activates once a reviewer marks the Identity Status field in the Notion case record as 'Confirmed'. The agent queries HubSpot via its API to retrieve all records associated with the requester, including contact properties, deal history, associated notes, and logged email history. It then compiles the retrieved data into a structured draft response package within the Notion case record, formats it according to the request type (SAR data export, erasure confirmation, or rectification summary), and sends a Slack notification to the assigned reviewer with a direct link to the case record and approval prompt. The agent does not send any external communication and does not proceed beyond Slack notification without explicit reviewer approval logged in Notion.
// Input (read from Notion case record on trigger)
notion.case_id // page ID of the active case
notion.requester_name // used as HubSpot search parameter
notion.requester_email // primary HubSpot lookup key
notion.request_type // SAR | erasure | rectification
notion.deadline_date // surfaced in Slack notification
notion.identity_status // must equal 'Confirmed' before execution
// HubSpot API queries (private app token, v3 CRM API)
GET /crm/v3/objects/contacts?email={requester_email} // resolve contact ID
GET /crm/v3/objects/contacts/{contact_id}?properties= // all contact properties
GET /crm/v3/objects/deals?associations.contact={id} // associated deals
GET /crm/v3/objects/notes?associations.contact={id} // associated notes
GET /crm/v3/objects/emails?associations.contact={id} // logged email history
// Output (written to Notion case record as draft response block)
notion.hubspot_contact_properties // JSON snapshot stored in case page
notion.hubspot_deals_summary // list of deal names, stages, values
notion.hubspot_notes_log // list of note bodies and dates
notion.hubspot_email_log // list of email subjects and timestamps
notion.draft_response_status // set to 'Awaiting Review'
notion.data_retrieved_timestamp // ISO 8601 timestamp of retrieval
// Output (Slack notification to reviewer)
slack.channel // #privacy-reviews or DM to assigned reviewer
slack.message // includes case_id, requester_name, request_type, deadline_date, Notion case URL
slack.sent_timestamp // recorded back to Notion case record
// On approval (reviewer sets Notion approval field to 'Approved')
gmail.to // requester_email
gmail.subject // dynamically set by request_type
gmail.body // compiled from notion draft response block
notion.case_status // updated to 'Closed'
notion.response_sent_timestamp // ISO 8601 timestamp
notion.reviewer_name // name of approving reviewer
notion.outcome // SAR sent | Data erased | Data corrected- HubSpot integration must use a Private App token, not a legacy API key. The token requires scopes: crm.objects.contacts.read, crm.objects.deals.read, crm.objects.notes.read, crm.objects.emails.read. Confirm the HubSpot account tier supports private apps (Starter and above). The token must be stored in the shared credential store and never hardcoded.
- Notion polling interval for the Identity Status field change should be set to 60 seconds or the shortest interval the plan supports. If the Notion API supports database-level webhooks (currently in limited beta), use a webhook instead of polling to reduce latency.
- HubSpot contact lookup must use requester_email as the primary key. If no contact is found, the agent must log a 'No CRM record found' entry in the Notion case record and send a Slack alert to the process owner, then halt execution for that step. It must not proceed to compile a blank response.
- The draft response structure in Notion must differ by request_type: SAR responses include all retrieved data categories; erasure requests generate a deletion confirmation checklist with fields for each data category; rectification requests surface only the fields the requester has asked to change alongside their current values.
- The Slack reviewer notification must include a direct URL to the Notion case page and the deadline date. The Slack workspace must have the automation platform's bot app installed with chat:write and channels:read scopes. Confirm the reviewer's Slack user ID or the target channel ID before build.
- The approval gate logic must be implemented so that the Gmail send step only fires when the Notion 'Reviewer Approval' property is set to 'Approved' by an authenticated Notion user. The platform workflow must poll this field after the Slack notification is sent. If approval is not received within 72 hours of the Slack notification, a reminder Slack message must be sent automatically.
- Data held outside HubSpot (billing platforms, support tools, offline records) cannot be retrieved automatically at initial rollout. The Notion draft response must include a clearly labelled section titled 'Manual data check required' listing any additional systems the process owner must check before the reviewer approves. This must be confirmed with the process owner before build.
- Fallback: if the HubSpot API returns a non-200 status code, the agent must retry up to three times, then log the failure to the error table and notify the process owner via Slack. The case must remain in 'In Progress' status until the data retrieval is resolved manually.
03End-to-end data flow
// ── TRIGGER ─────────────────────────────────────────────────────────────────
// Event: Typeform 'Privacy Request Intake' form submitted
TYPEFORM_WEBHOOK_PAYLOAD {
form_response.answers.requester_full_name // string
form_response.answers.requester_email // string
form_response.answers.request_type // 'SAR' | 'erasure' | 'rectification'
form_response.answers.additional_context // string, optional
form_response.submitted_at // ISO 8601
}
// ── AGENT 1: INTAKE AND TRIAGE AGENT ────────────────────────────────────────
// Step A: Dedupe check against Notion cases database
NOTION_QUERY {
filter: requester_email == TYPEFORM.requester_email
AND case_status IN ['Open', 'In Progress']
AND submission_date >= NOW() - 60 days
result: if match found -> SLACK_ALERT(duplicate) -> HALT
if no match -> CONTINUE
}
// Step B: Create Notion case record
NOTION_DATABASE_CREATE {
Case ID <- auto-generated Notion page ID
Requester Name <- TYPEFORM.requester_full_name
Requester Email <- TYPEFORM.requester_email
Request Type <- TYPEFORM.request_type
Submission Date <- TYPEFORM.submitted_at
Deadline Date <- TYPEFORM.submitted_at + 30 days
Identity Status <- 'Pending'
Case Status <- 'Open'
}
// Step C: Send identity verification email via Gmail
GMAIL_SEND {
to <- TYPEFORM.requester_email
subject <- 'Your privacy request has been received - action required'
body <- TEMPLATE(request_type, deadline_date, id_requirements)
on_send -> NOTION_UPDATE { Verification Email Sent: true, Sent Timestamp: NOW() }
}
// ── HUMAN GATE 1: IDENTITY VERIFICATION ─────────────────────────────────────
// Reviewer examines returned ID documents and updates Notion manually
// NOTION.identity_status set to 'Confirmed' | 'Failed'
// If 'Failed': process owner sends follow-up email (manual exception path)
// If 'Confirmed': triggers Agent 2
// ── AGENT 2: DATA DISCOVERY AGENT ───────────────────────────────────────────
// Trigger: NOTION.identity_status == 'Confirmed' (polling / webhook)
// Step D: Resolve requester in HubSpot
HUBSPOT_API_GET /crm/v3/objects/contacts {
filter_groups[0].filters[0].propertyName: 'email'
filter_groups[0].filters[0].value: NOTION.requester_email
result: contact_id // if null -> NOTION_UPDATE('No CRM record found') -> SLACK_ALERT -> HALT
}
// Step E: Retrieve all CRM data for contact
HUBSPOT_API_GET /crm/v3/objects/contacts/{contact_id} {
properties: firstname, lastname, email, phone, createdate, hs_lead_status
result: contact_properties
}
HUBSPOT_API_GET /crm/v3/objects/deals?associations.contact={contact_id} {
result: deals_list [ deal_name, deal_stage, amount, closedate ]
}
HUBSPOT_API_GET /crm/v3/objects/notes?associations.contact={contact_id} {
result: notes_list [ body, createdate, owner ]
}
HUBSPOT_API_GET /crm/v3/objects/emails?associations.contact={contact_id} {
result: email_log [ subject, sent_at, direction ]
}
// Step F: Compile draft response in Notion
NOTION_PAGE_UPDATE (case_id) {
hubspot_contact_properties <- contact_properties (JSON block)
hubspot_deals_summary <- deals_list
hubspot_notes_log <- notes_list
hubspot_email_log <- email_log
manual_check_section <- ['Billing system', 'Support platform', 'Offline records']
draft_response_status <- 'Awaiting Review'
data_retrieved_timestamp <- NOW()
}
// Step G: Notify reviewer via Slack
SLACK_MESSAGE {
channel <- #privacy-reviews
text <- TEMPLATE(case_id, requester_name, request_type, deadline_date, notion_case_url)
on_send -> NOTION_UPDATE { Reviewer Notified Timestamp: NOW() }
}
// If NOTION.reviewer_approval != 'Approved' within 72 hours:
SLACK_MESSAGE { text: 'Reminder: case {case_id} approval still pending. Deadline: {deadline_date}' }
// ── HUMAN GATE 2: REVIEWER APPROVAL ─────────────────────────────────────────
// Reviewer reads Notion draft, annotates if needed, sets Reviewer Approval: 'Approved'
// Platform polls NOTION.reviewer_approval field
// If 'Approved' -> CONTINUE
// If annotated -> process owner revises draft -> resets field -> re-notifies Slack
// Step H: Send approved response via Gmail
GMAIL_SEND {
to <- NOTION.requester_email
subject <- TEMPLATE(request_type) // e.g. 'Your Subject Access Request response'
body <- compiled from NOTION draft response block
on_send -> NOTION_UPDATE { Response Sent Timestamp: NOW() }
}
// Step I: Close case and update audit log in Notion
NOTION_PAGE_UPDATE (case_id) {
Case Status <- 'Closed'
Response Sent Timestamp <- NOW()
Reviewer Name <- NOTION.last_editor_name
Outcome <- TEMPLATE(request_type) // 'SAR sent' | 'Data erased' | 'Data corrected'
}
// ── END OF FLOW ──────────────────────────────────────────────────────────────04Recommended build stack
More documents for this process
Every document generated for GDPR / Data Privacy Request Handling.