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
Employee Satisfaction and Pulse Surveys
[YourCompany.com] · HR Department · Prepared by FullSpec · [Today's Date]
This document is the primary technical reference for the FullSpec team building the Employee Satisfaction and Pulse Survey automation. It covers the current-state process map, full agent specifications with IO contracts, end-to-end data flow, and the recommended build stack. Everything needed to build, connect, and validate both agents without ambiguity is contained here. Where confirmation is still needed before build (API tiers, OAuth scopes, sending limits), those items are called out explicitly in the relevant agent specification.
01Process snapshot
02Agent specifications
Handles the complete send-and-chase cycle for each survey window. On the scheduled trigger date (or when a BambooHR new-hire reaches their 30-day mark), the agent queries the BambooHR API for the current active employee list, filters out contractors and leavers by employment status field, and sends personalised Gmail invitations containing the Typeform survey URL. All invited employees are logged to a Google Sheets tracking tab with their email, invite timestamp, and response status. After a configurable interval (default 3 days before survey close), the agent queries Typeform for submitted responses, cross-references against the invited list in Google Sheets, and sends a single reminder email via Gmail to each non-responder. The Slack HR channel receives a real-time response rate alert after the reminder batch fires. The agent is designed to send reminders exactly once per employee per cycle: the Google Sheets log acts as the dedupe source of truth.
// Input trigger_type: 'scheduled' | 'new_hire_30day' survey_cycle_id: string // e.g. '2025-02-W1' typeform_form_id: string // confirmed Typeform form ID reminder_offset_days: integer // days before close to send reminder (default: 3) // BambooHR employee record fields consumed employee.id: string employee.firstName: string employee.lastName: string employee.workEmail: string employee.employmentStatus: string // filter: 'Active' only employee.department: string // Output (written to Google Sheets: tab 'Distribution_Log') cycle_id: string employee_id: string email: string department: string invite_sent_at: ISO8601 timestamp reminder_sent_at: ISO8601 timestamp | null response_status: 'invited' | 'responded' | 'reminder_sent' typeform_response_id: string | null // Slack output (HR channel) message: 'Survey [cycle_id] reminder sent. Response rate: [n]/[total] ([pct]%)' // On error error.step: string // e.g. 'bamboohr_fetch' | 'gmail_send' | 'typeform_check' error.message: string error.employee_id: string | null // if failure is record-specific // Failed sends logged to Google Sheets tab 'Error_Log'; Slack alert fires to HR channel
- BambooHR API access: confirm the account API key is generated from an admin account and that the plan tier permits API calls (some lower-tier BambooHR plans restrict or rate-limit API access to the employee directory endpoint). Endpoint to use: GET /v1/employees/directory with fields filter for id, firstName, lastName, workEmail, department, employmentStatus.
- Gmail sending: use the Gmail API (OAuth 2.0, scope mail.send) from the verified sending account. Confirm daily send limit is sufficient for the employee count (standard Gmail API limit is 500 recipients/day for regular accounts; Google Workspace accounts permit up to 2,000/day). Batch sends in groups of 50 with a 2-second delay to avoid triggering spam filters.
- Typeform response check: use the Typeform Responses API (GET /forms/{form_id}/responses) with the submitted_at filter to identify who has responded. Partially completed responses are NOT returned by this endpoint and must be treated as non-responses for reminder purposes.
- Dedupe rule: before sending any reminder, check the Google Sheets Distribution_Log for reminder_sent_at. If a non-null value exists for that employee_id in the current cycle_id, skip the reminder send entirely. This is the only dedupe gate.
- Reminder timing: configurable via the reminder_offset_days parameter. Default is 3 days before survey close. This value must be confirmed with the HR Manager before build and stored as a workflow-level variable, not hard-coded.
- New-hire 30-day trigger: the BambooHR API does not natively emit a webhook for date-based field changes. Implement as a daily scheduled poll (each morning at 08:00 local time) that queries BambooHR for employees whose hireDate is exactly 30 days ago and triggers the survey flow for those individuals only. Confirm the hireDate field name in the connected BambooHR instance before build.
- Confirm the specific Typeform form ID for the active pulse survey before build. The form ID is embedded in the Typeform share URL and must be stored as a workflow-level credential variable.
Fires automatically when the survey window end date is reached, or when HR manually closes the survey in Typeform (detected via a Typeform webhook or a scheduled check). The agent fetches all submitted responses from Typeform, writes them into a structured Google Sheets tab (one row per respondent), and applies pre-built scoring formulas to calculate eNPS (detractors, passives, promoters), category average scores, and department-level breakdowns. It then reads the current cycle scores alongside the prior cycle scores (stored in a separate Google Sheets tab named 'Score_History') to identify meaningful shifts. An AI language model call then receives the scored data as structured input and generates a plain-English narrative summary covering key themes, score shifts, and any department scores that fall below the configured threshold (default: category average below 6.0 on a 10-point scale). The narrative and scored data are written into a new Notion page using the HR Report template. The agent sets the Notion page status to 'Pending Review'. No Slack or Gmail distribution fires until HR has reviewed and approved the Notion page, at which point a separate approval-triggered step posts the report link to Slack and sends it via Gmail to the leadership distribution list.
// Input (from Typeform Responses API)
form_id: string
response.response_id: string
response.submitted_at: ISO8601 timestamp
response.hidden_fields.employee_id: string // set via Typeform hidden field
response.answers[].field.ref: string // question reference slug
response.answers[].number: integer // numeric score (1-10 scale)
// Google Sheets input (tab 'Score_History')
prior_cycle.cycle_id: string
prior_cycle.category_averages: object // {category_name: float}
prior_cycle.enps_score: float
// AI model input (structured JSON payload)
current_cycle_id: string
enps_score: float
category_averages: object // {category_name: float}
flagged_departments: array // departments below threshold
score_shifts: object // {category_name: delta_float}
response_rate_pct: float
total_responses: integer
// Output (written to Google Sheets: tab 'Responses_{cycle_id}')
employee_id: string
response_id: string
submitted_at: ISO8601 timestamp
department: string
enps_category: 'Promoter' | 'Passive' | 'Detractor'
score_{category_ref}: float // one column per category
// Output (written to Google Sheets: tab 'Score_History')
cycle_id, enps_score, response_rate_pct, category_averages (appended row)
// Output (Notion page created from HR Report template)
page.title: 'Survey Report: {cycle_id}'
page.status: 'Pending Review'
page.properties.cycle_id: string
page.properties.response_rate: float
page.properties.enps_score: float
page.blocks: narrative text + score table + flagged_departments list
// On approval (HR sets Notion page status to 'Approved')
// Approval detection: poll Notion page status every 15 min, OR use Notion webhook if available
slack.channel: '#hr-reports'
slack.message: 'Survey report for {cycle_id} is ready. [{response_rate_pct}% response rate, eNPS: {enps_score}] [Notion link]'
gmail.to: leadership_distribution_list // stored as workflow variable
gmail.subject: 'Employee Survey Report: {cycle_id}'
gmail.body: narrative_summary + notion_page_url
// On error
error.step: string // e.g. 'typeform_fetch' | 'sheets_write' | 'notion_create' | 'ai_call'
error.message: string
// Slack alert to HR channel; error logged to Google Sheets tab 'Error_Log'- Typeform plan requirement: the Typeform webhook for 'form_closed' events requires a Business plan or above. Confirm the current Typeform plan before build. If the plan is lower, implement a scheduled end-of-window check (cron at the configured survey close datetime) as the fallback trigger instead of the webhook.
- Typeform hidden fields: to link a Typeform response back to a specific BambooHR employee_id, the survey invitation URL must include a hidden field parameter (e.g. ?employee_id=EMP-001). The Survey Distribution Agent must append this parameter to each personalised survey link before sending. Confirm this is correctly implemented in Agent 1 before testing Agent 2.
- Google Sheets schema: the 'Responses_{cycle_id}' tab must be pre-created (or created dynamically) with the correct column headers before rows are written. Define the canonical column schema in the workflow credential store and use it as the schema source for all write operations.
- AI narrative generation: use a GPT-4-class model via API. The prompt must instruct the model to write in plain English suitable for non-technical HR leadership, to flag any category below the configured threshold (default: 6.0/10), and to note score shifts greater than 0.5 points versus the prior cycle. The prompt and threshold values must be stored as editable workflow variables, not embedded in the code.
- Notion template: a named Notion template page titled 'HR Survey Report Template' must exist in the connected Notion workspace before build. The agent creates each new report by duplicating this template. Confirm the template page ID in the Notion workspace and store it as a credential variable.
- Approval detection: Notion does not natively emit webhooks for property changes on standard plans. Implement approval detection as a scheduled poll (every 15 minutes) that checks the Notion page's 'Status' property for the value 'Approved'. If Notion webhooks become available on the connected plan, switch to event-driven detection. Confirm the Notion plan tier before build.
- Sensitive score handling: if any flagged department involves questions related to management ratings or pay satisfaction (identified by question field.ref slug), append a note to the Notion page body instructing HR to review these sections before approving. Do not suppress the data; flag it inline.
- The leadership distribution list (Gmail recipients for the final send) must be stored as a workflow-level variable, not hard-coded. Confirm the list with the HR Manager before go-live.
03End-to-end data flow
// ============================================================
// TRIGGER
// ============================================================
TRIGGER: cron_schedule ('0 8 * * 1' for weekly pulse) | new_hire_30day_poll
-> emit: { trigger_type, survey_cycle_id, typeform_form_id, reminder_offset_days }
// ============================================================
// AGENT 1: Survey Distribution Agent
// ============================================================
// Step 1: Fetch active employees from BambooHR
GET bamboohr.com/api/gateway.php/{subdomain}/v1/employees/directory
params: { fields: 'id,firstName,lastName,workEmail,department,employmentStatus' }
filter: employmentStatus == 'Active'
-> emit: employees[]
{ employee.id, employee.firstName, employee.lastName,
employee.workEmail, employee.department }
// Step 2: Send personalised Gmail invitations
FOR EACH employee IN employees[]:
survey_url = typeform_base_url + '?employee_id=' + employee.id
POST gmail.users.messages.send
{ to: employee.workEmail,
subject: 'We want your feedback - [cycle_id] survey',
body: personalised_template(employee.firstName, survey_url) }
-> log to Google Sheets tab 'Distribution_Log':
{ cycle_id, employee_id, email, department,
invite_sent_at: NOW(), response_status: 'invited',
reminder_sent_at: null, typeform_response_id: null }
// Step 3: Reminder check (fires at survey_close_date minus reminder_offset_days)
GET api.typeform.com/forms/{typeform_form_id}/responses
params: { completed: true, page_size: 1000 }
-> emit: responded_ids[] = [response.hidden_fields.employee_id]
FOR EACH employee IN employees[]:
IF employee.id NOT IN responded_ids[]
AND Distribution_Log[employee.id, cycle_id].reminder_sent_at IS NULL:
POST gmail.users.messages.send // single reminder
{ to: employee.workEmail, subject: 'Reminder: survey closes soon' }
UPDATE Distribution_Log: { reminder_sent_at: NOW(),
response_status: 'reminder_sent' }
// Step 4: Post response rate alert to Slack
POST slack.chat.postMessage
{ channel: '#hr-surveys',
text: 'Survey [cycle_id] reminder sent. Rate: [responded]/[total] ([pct]%)' }
// ============================================================
// AGENT 1 -> AGENT 2 HANDOFF
// Trigger: survey window end datetime reached (cron) OR
// Typeform 'form_closed' webhook event received
// Payload passed: { survey_cycle_id, typeform_form_id }
// ============================================================
// ============================================================
// AGENT 2: Response Analysis Agent
// ============================================================
// Step 5: Fetch all submitted responses from Typeform
GET api.typeform.com/forms/{typeform_form_id}/responses
params: { completed: true, page_size: 1000 }
-> emit: responses[]
{ response_id, submitted_at,
hidden_fields.employee_id,
answers[].field.ref,
answers[].number }
// Step 6: Write response rows to Google Sheets
FOR EACH response IN responses[]:
APPEND to Google Sheets tab 'Responses_{cycle_id}':
{ employee_id, response_id, submitted_at, department,
score_{category_ref} per answer,
enps_category: classify(enps_answer) }
// Step 7: Calculate scores and eNPS using Sheets formulas
// Pre-built named ranges and ARRAYFORMULA blocks handle:
// - eNPS = (promoters/total - detractors/total) * 100
// - category_average = AVERAGEIF(score_col, cycle_id)
// - department_breakdown = AVERAGEIFS per department
-> read calculated values:
{ enps_score: float, category_averages: {}, department_averages: {},
response_rate_pct: float, total_responses: integer }
// Step 8: Read prior cycle from Score_History tab
GET Google Sheets tab 'Score_History' -> filter last row by prior cycle_id
-> emit: { prior_enps_score, prior_category_averages }
// Step 9: Compute score shifts
score_shifts = { category: current_avg - prior_avg } per category
flagged_departments = [dept WHERE dept_avg < threshold (default 6.0)]
// Step 10: AI narrative generation
POST openai.com/v1/chat/completions // GPT-4-class model
body: { model, messages: [system_prompt, user_payload] }
user_payload: { cycle_id, enps_score, category_averages,
score_shifts, flagged_departments,
response_rate_pct, total_responses }
-> emit: narrative_text: string
// Step 11: Create Notion report page
POST api.notion.com/v1/pages
{ parent: { page_id: notion_template_page_id },
properties: {
title: 'Survey Report: {cycle_id}',
Status: 'Pending Review',
CycleID: cycle_id,
ResponseRate: response_rate_pct,
eNPS: enps_score },
children: [narrative_text blocks, score_table, flagged_departments list] }
-> emit: notion_page_id, notion_page_url
// Step 12: Append to Score_History
APPEND to Google Sheets tab 'Score_History':
{ cycle_id, enps_score, response_rate_pct, category_averages, timestamp: NOW() }
// ============================================================
// APPROVAL GATE: HR reviews Notion page
// Poll every 15 min: GET api.notion.com/v1/pages/{notion_page_id}
// check: properties.Status.select.name == 'Approved'
// Approval detected -> trigger distribution step
// ============================================================
// Step 13: On approval - post to Slack
POST slack.chat.postMessage
{ channel: '#hr-reports',
text: 'Survey report [{cycle_id}] approved. eNPS: {enps_score}. Rate: {response_rate_pct}%. {notion_page_url}' }
// Step 14: On approval - send via Gmail to leadership
POST gmail.users.messages.send
{ to: leadership_distribution_list[],
subject: 'Employee Survey Report: {cycle_id}',
body: narrative_summary_excerpt + notion_page_url }
// ============================================================
// ERROR HANDLING (all agents)
// On any step failure:
// APPEND to Google Sheets tab 'Error_Log':
// { timestamp, agent, step, error_message, employee_id | null }
// POST slack.chat.postMessage to '#hr-automation-alerts'
// ============================================================04Recommended build stack
More documents for this process
Every document generated for Employee Satisfaction & Pulse Surveys.