Back to Employee Satisfaction & Pulse Surveys

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

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

Step
Name
Description
1
Pull Active Employee List from HRIS
HR logs into BambooHR and manually exports the current active employee list, filtering contractors and leavers. Used to build the distribution group. Cost: 20 min/cycle.
2
Design or Update Survey Questions
HR reviews and edits the Typeform survey to reflect new themes or business priorities. Full redesigns extend this significantly. Cost: 30 min/cycle.
3
Send Initial Survey Invitations
HR copies email addresses into Gmail and sends the survey link in batches. Individual personalisation is rare due to time pressure. Cost: 25 min/cycle.
4
Monitor Response Rate Daily
HR checks Typeform each day and logs a rough tally in a spreadsheet. No automated way to identify specific non-responders without manual cross-referencing. Cost: 15 min/day across the window.
5
Send Manual Reminder Emails
HR compares the employee list against Typeform submissions to find non-responders, then writes and sends reminder emails individually or in small batches. Cost: 30 min/cycle.
6
Export and Clean Response Data
HR exports the CSV from Typeform and manually cleans it, removing test entries and reformatting columns. Error-prone at higher employee counts. Cost: 25 min/cycle.
7
Calculate Summary Scores and Trends
HR builds or updates formulas in Google Sheets for average scores, eNPS, and department breakdowns. Charts are recreated for each cycle. Cost: 40 min/cycle.
8
Write Narrative Summary for Leadership
HR drafts a written summary of key themes, concerning scores, and notable shifts in Notion. Typically requires two or three revision passes. Cost: 45 min/cycle.
9
Share Report with Leadership via Email
HR emails the completed report and data summary to the relevant managers and leadership team. Cost: 10 min/cycle.
Time cost summary: Total manual time per cycle is 240 minutes (4 hours). At 4 survey cycles per month (weekly pulse), that equates to approximately 16 hours per month and 5 hours per week in sustained overhead. The automation replaces steps 1, 3, 4, 5 (Survey Distribution Agent) and steps 6, 7, 8 (Response Analysis Agent). Steps 2 (question design) and 9 (final report share, now handled by Slack and Gmail post-approval) remain as lightweight human tasks. Post-automation HR time per cycle drops to under 30 minutes, consisting of the Notion report review only.
Developer Handover PackPage 1 of 4
FS-DOC-04Technical

02Agent specifications

Survey Distribution Agent

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.

Trigger
Scheduled date/time (cron) for recurring pulse cycle, OR BambooHR webhook/poll detecting a new hire's 30-day anniversary field change
Tools
BambooHR, Gmail, Typeform, Google Sheets, Slack
Replaces steps
1 (employee list pull), 3 (invitation send), 4 (daily response monitoring), 5 (manual reminder emails)
Estimated build
14 hours, Moderate complexity
// 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.
Response Analysis Agent

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.

Trigger
Scheduled check at survey window end datetime, OR Typeform 'form_closed' webhook event (requires Typeform Business plan or above)
Tools
Typeform, Google Sheets, Notion, Slack, Gmail
Replaces steps
6 (export and clean response data), 7 (calculate summary scores and trends), 8 (write narrative summary for leadership)
Estimated build
14 hours, Moderate complexity
// 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.
Developer Handover PackPage 2 of 4
FS-DOC-04Technical

03End-to-end data flow

Full trigger-to-output data flow, Employee Satisfaction and Pulse Survey automation
// ============================================================
// 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'
// ============================================================
Developer Handover PackPage 3 of 4
FS-DOC-04Technical

04Recommended build stack

Orchestration layer
A workflow automation platform with native HTTP/REST node support. Build one workflow per agent (Survey Distribution Agent and Response Analysis Agent), plus one shared utility workflow for error logging and Slack alerts. Store all credentials (BambooHR API key, Gmail OAuth 2.0 tokens, Typeform API token, Notion integration token, Slack bot token, OpenAI API key) in the platform's shared credential store, not in individual node configurations. Credential names must match the naming convention defined in the Integration and API Spec (Doc 05).
Webhook configuration
Agent 1 primary trigger: cron schedule (configurable, default '0 8 * * 1' for weekly pulse). Agent 1 secondary trigger: daily poll of BambooHR for 30-day new-hire anniversaries ('0 8 * * *'). Agent 2 primary trigger: Typeform 'form_response' webhook on form closure event (requires Typeform Business plan). Agent 2 fallback trigger: cron at the configured survey_close_datetime. The approval detection loop in Agent 2 runs as a sub-workflow polling the Notion page status property every 15 minutes. All inbound webhooks must be secured with a shared secret header (X-Webhook-Secret) validated at the workflow entry node.
Templating approach
Gmail invitation and reminder bodies are stored as named text templates in the workflow credential store with handlebar-style placeholders ({{firstName}}, {{survey_url}}, {{cycle_id}}). The AI narrative prompt is stored as a workflow-level variable to allow HR to request prompt adjustments without a code change. The Notion report structure is defined by a master template page in the Notion workspace. Google Sheets tabs use a pre-defined column schema stored as a JSON schema variable in the orchestration layer, which is referenced by all write operations to prevent schema drift across cycles.
Error logging
All agent errors are written to a dedicated Google Sheets tab named 'Error_Log' with columns: timestamp (ISO8601), agent_name, step_name, error_message, affected_employee_id (null if not record-specific), cycle_id, resolved (boolean, default FALSE). On any error write, the shared utility workflow fires a Slack alert to the '#hr-automation-alerts' channel including the step name and a link to the Error_Log tab. Critical failures (BambooHR fetch failure, Typeform API 5xx, Notion page creation failure) also trigger a Gmail alert to the designated FullSpec support contact (support@gofullspec.com) and the HR Manager.
Testing approach
All agent builds are tested in sandbox-first mode before any production credentials are connected. Phase 1: use BambooHR sandbox environment and a Typeform test form with a small internal group (5 to 10 test employees using internal email aliases). Phase 2: run two full simulated survey cycles end to end, validating the reminder dedupe logic, eNPS formula accuracy against manual calculations, score shift detection, AI narrative output quality, and Notion page creation. Phase 3: HR reviews AI narrative output and confirms the threshold flagging logic before any go-live cycle. Production credentials are connected only after all Phase 2 test cases in the Test and QA Plan (Doc 06) have passed.
Estimated total build time
Survey Distribution Agent: 14 hours. Response Analysis Agent: 14 hours. End-to-end integration testing and QA (two full simulated cycles, error handling validation, AI narrative review, approval gate testing): 8 hours (included in the 28-hour total per the build specification). Remaining 2 hours allocated to credential setup, schema definition, and documentation updates. Grand total: 28 hours across approximately 3 to 4 business weeks, aligned to the confirmed delivery schedule.
Before build begins, the following must be confirmed and recorded in the credential store: (1) BambooHR API key from an admin account, and confirmation that the connected plan permits directory API calls. (2) Typeform plan tier: Business plan required for webhook-based close detection. (3) The specific Typeform form ID for the active pulse survey. (4) Notion integration token and the template page ID for HR Survey Reports. (5) Gmail OAuth 2.0 credentials with scope mail.send, and confirmation of the daily send limit for the connected Google Workspace account. (6) Leadership distribution list for the final report send step. (7) Slack bot token and the confirmed channel names for HR alerts (#hr-surveys, #hr-reports, #hr-automation-alerts). Contact support@gofullspec.com if any credential or plan access issue is encountered during setup.
Developer Handover PackPage 4 of 4

More documents for this process

Every document generated for Employee Satisfaction & Pulse Surveys.

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