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
Performance Review Cycle
[YourCompany.com] · HR Department · Prepared by FullSpec · [Today's Date]
This document is the primary technical reference for the FullSpec team building and configuring the Performance Review Cycle automation. It covers the current-state process map with bottleneck identification, full agent specifications with IO definitions, the end-to-end data flow trace, and the recommended build stack. Everything needed to configure, connect, and test all three agents is contained here. Where credentials, API tiers, or form structures are still to be confirmed, those items are flagged as pre-build blockers inside the relevant agent specification.
01Process snapshot
02Agent specifications
Reads the confirmed active employee list from BambooHR via API at the moment the review cycle is marked open, resolves the employee-to-manager mapping, generates personalised email content for each recipient using a stored template, dispatches kick-off notifications via Gmail, and posts channel and direct-message alerts in Slack. No manual drafting or list preparation is required. Estimated build time: 10 hours. Complexity: Moderate.
// Input
BambooHR.review_cycle_status == 'open'
BambooHR.employees[] -> { employee_id, full_name, email, manager_id, manager_email, department }
BambooHR.review_cycle -> { cycle_name, submission_deadline, self_assessment_form_url, manager_form_url }
// Output
Gmail.send_batch -> employees[]: { to: employee.email, subject: 'Your performance review is open', body: templated(employee.full_name, self_assessment_form_url, submission_deadline) }
Gmail.send_batch -> managers[]: { to: manager.email, subject: 'Manager briefing: review cycle open', body: templated(manager.full_name, direct_reports[], manager_form_url, submission_deadline) }
Slack.post_message -> channel: #hr-announcements, text: 'Review cycle [cycle_name] is now open. Deadline: [submission_deadline].'
Slack.post_dm -> managers[]: { user: manager.slack_id, text: 'Your team review forms are live. Check your email for links.' }
tracker_sheet.initialise_rows -> employees[]: { employee_id, full_name, manager_name, self_assessment_status: 'Pending', manager_assessment_status: 'Pending', last_reminder_sent: null }- BambooHR API tier must support third-party API access. Confirm the account plan before build; some smaller tiers require an upgrade. The API key should be stored in the shared credential store, not hardcoded.
- The employee-manager mapping is read from BambooHR's standard employeeId and supervisorId fields. If the org has matrix reporting or interim managers, a supplementary mapping override table in Google Sheets must be agreed and loaded before the agent runs.
- Gmail dispatch uses a single authenticated service account with domain-wide delegation enabled for the Google Workspace domain. The sending address should be confirmed with the HR team (e.g. hr-reviews@[YourCompany.com]) before build.
- Slack integration requires the bot token with chat:write and users:read scopes. Confirm whether Slack user IDs are stored in BambooHR or whether a separate email-to-Slack-ID lookup table is needed.
- The kick-off email template and manager briefing template must be signed off by the HR team before build. Store templates as variables in the orchestration layer, not inside the email tool.
- Dedupe guard: if the trigger fires twice (e.g. a webhook retry), the agent must check whether tracker rows already exist for the current cycle_name before re-sending emails. Use an idempotency key composed of cycle_name + employee_id.
Continuously monitors Google Forms for new responses by polling the linked Google Sheet at a configurable interval (default every 4 hours). On each poll it updates the submission status for each employee and manager in the master tracker. On a daily schedule it identifies rows where status is still Pending and the deadline has not passed, then dispatches reminder emails via Gmail and Slack nudges. A final 48-hour deadline warning is sent to all outstanding submitters regardless of prior reminders. Estimated build time: 14 hours. Complexity: Moderate.
// Input
Google Forms (via linked Google Sheet): responses_sheet.getNewRows() -> { response_timestamp, employee_id, form_type: 'self_assessment' | 'manager_assessment', respondent_email }
tracker_sheet.rows[] -> { employee_id, full_name, manager_email, self_assessment_status, manager_assessment_status, last_reminder_sent, reminder_count }
cycle_config -> { submission_deadline, reminder_threshold_hours: 24, max_reminders: 3 }
// Output (on each poll)
tracker_sheet.update_row -> { employee_id, self_assessment_status: 'Submitted' | 'Pending', manager_assessment_status: 'Submitted' | 'Pending', last_updated: now() }
// Output (on daily reminder job)
IF row.self_assessment_status == 'Pending' AND row.reminder_count < max_reminders:
Gmail.send -> { to: employee.email, subject: 'Reminder: self-assessment due [deadline]', body: templated(employee.full_name, submission_deadline, self_assessment_form_url) }
Slack.post_dm -> { user: employee.slack_id, text: 'Friendly reminder: your self-assessment is due [deadline].' }
tracker_sheet.update_row -> { last_reminder_sent: now(), reminder_count: reminder_count + 1 }
// Output (48-hour deadline warning, sent once)
IF hours_until_deadline <= 48 AND row.status == 'Pending' AND final_warning_sent == false:
Gmail.send -> { to: employee.email, subject: 'URGENT: review form due in 48 hours', body: templated(...) }
tracker_sheet.update_row -> { final_warning_sent: true }- Google Forms must be configured to write responses to a linked Google Sheet (standard Forms setting). The sheet name and tab name must be confirmed before build; the agent reads from a named tab, not by index.
- The response sheet must include an employee_id or email column that can be matched back to the tracker. If the current form design does not capture this, a hidden pre-filled field or a unique form URL per employee must be implemented before build.
- The reminder threshold (24 hours before sending first reminder) and max_reminders (default 3) are stored as configurable variables in the orchestration layer so the HR team can adjust them without code changes.
- If peer review feedback is in scope, the matching logic must be extended to track a third form type per employee. This adds estimated build time; confirm scope before starting this agent.
- Dedupe guard: the poll step must record the last processed response row index or timestamp to avoid processing the same response twice across polling windows.
- The daily reminder job and the 4-hour poll are separate scheduled triggers. Both must be paused automatically once the submission_deadline has passed to prevent post-deadline reminders firing.
Listens for the moment both the employee self-assessment and the manager assessment have been submitted for the same employee (matched on employee_id). Once a matched pair is detected in the tracker, the agent reads both sets of form responses, applies a structured Google Docs template, and uses an AI drafting step to produce a coherent one-page narrative summary. The completed document is saved to the manager's designated Google Drive folder and a Gmail notification with a direct link is sent to the manager. Estimated build time: 14 hours. Complexity: Complex.
// Input
tracker_sheet.matched_pair -> { employee_id, employee_name, manager_id, manager_name, manager_email, manager_drive_folder_id }
self_assessment_responses -> { question_1_response, question_2_response, ..., overall_rating_self, key_achievements, development_areas }
manager_assessment_responses -> { question_1_response, question_2_response, ..., overall_rating_manager, strengths_observed, areas_for_growth, recommended_action }
summary_template_doc_id -> Google Docs template ID (stored in config)
// AI drafting step (intermediate)
ai_prompt -> { system: 'You are an HR assistant. Produce a structured one-page performance summary.', input: { employee_name, self_assessment_responses, manager_assessment_responses, template_sections: ['Overview', 'Achievements', 'Development Areas', 'Manager Perspective', 'Agreed Next Steps'] } }
ai_output -> { summary_text_per_section{} }
// Output
Google Docs.copy_template(summary_template_doc_id) -> new_doc_id
Google Docs.populate_placeholders(new_doc_id) -> { {{EMPLOYEE_NAME}}, {{CYCLE_NAME}}, {{OVERVIEW}}, {{ACHIEVEMENTS}}, {{DEVELOPMENT_AREAS}}, {{MANAGER_PERSPECTIVE}}, {{AGREED_NEXT_STEPS}} }
Google Drive.move(new_doc_id) -> manager_drive_folder_id
Google Drive.set_permissions(new_doc_id) -> { role: 'writer', emailAddress: manager.email }
Gmail.send -> { to: manager.email, subject: 'Summary ready: [employee_name] — [cycle_name]', body: templated(manager.full_name, employee_name, doc_url) }
tracker_sheet.update_row -> { summary_status: 'Draft Ready', summary_doc_url: doc_url, summary_generated_at: now() }
// On approval (post-calibration, human-gated)
HR_approval_flag -> tracker_sheet row: final_rating_approved == true AND final_rating_value set
BambooHR.update_employee_record -> { employee_id, performance_rating: final_rating_value, review_cycle: cycle_name, review_notes: summary_doc_url }- The Google Docs template must be created and approved by the HR team before this agent is built. The template must use double-curly-brace placeholder syntax (e.g. {{EMPLOYEE_NAME}}) consistently across all sections so the population step can target them reliably.
- The AI drafting step uses a configured LLM API call (model and endpoint stored in the credential store). The prompt must be reviewed and iterated against real sample form responses before go-live. Summary quality is directly dependent on the structure and completeness of the Google Form questions.
- The manager Google Drive folder structure must be confirmed and pre-created (or auto-created by this agent using the Drive API) before the first summary is generated. The folder ID per manager must be stored in the tracker or a config sheet.
- The BambooHR write-back in the 'On approval' path fires only after a human sets final_rating_approved to true in the tracker. This is a gated step and must never fire automatically. Implement an explicit approval column in the tracker that defaults to false.
- Dedupe guard: check whether a summary_doc_url already exists for the employee_id before generating a new document. If a doc already exists, log the duplicate trigger event and skip rather than overwriting.
- Confirm BambooHR API write permissions: the API key in use must have the performance_reviews:write scope. Read-only keys will not support the final ratings write-back.
03End-to-end data flow
// ============================================================
// PERFORMANCE REVIEW CYCLE: END-TO-END DATA FLOW
// ============================================================
// TRIGGER
// BambooHR emits event or polling detects: review_cycle_status == 'open'
BambooHR.event -> {
cycle_name: string,
cycle_id: string,
submission_deadline: ISO8601_date,
self_assessment_form_url: string,
manager_form_url: string
}
// ============================================================
// AGENT 1: CYCLE LAUNCH AGENT
// ============================================================
// Step 1: Pull active employee roster from BambooHR
BambooHR.GET /v1/employees?status=active ->
employees[]: {
employee_id: string,
full_name: string,
email: string,
slack_id: string, // from BambooHR custom field or lookup table
manager_id: string,
manager_name: string,
manager_email: string,
manager_slack_id: string,
department: string
}
// Step 2: Initialise master tracker in Google Sheets
Google Sheets.appendRows(tracker_sheet_id, tab: 'Submissions') ->
per employee: {
employee_id, full_name, department, manager_name, manager_email,
self_assessment_status: 'Pending',
manager_assessment_status: 'Pending',
last_reminder_sent: null,
reminder_count: 0,
final_warning_sent: false,
summary_status: 'Not Started',
summary_doc_url: null,
final_rating_approved: false,
final_rating_value: null
}
// Step 3: Send Gmail kick-off emails (employees)
Gmail.send_batch ->
to: employee.email
subject: 'Your performance review is open — [cycle_name]'
body_vars: { full_name, self_assessment_form_url, submission_deadline }
// Step 4: Send Gmail manager briefing emails
Gmail.send_batch ->
to: manager.email (deduplicated across direct reports)
subject: 'Manager briefing: review cycle open — [cycle_name]'
body_vars: { manager_name, direct_reports[], manager_form_url, submission_deadline }
// Step 5: Post Slack notifications
Slack.post_message ->
channel: #hr-announcements
text: 'Review cycle [cycle_name] is now open. Deadline: [submission_deadline].'
Slack.post_dm (per manager) ->
user: manager.slack_id
text: 'Your team review forms are live. Check your email for links.'
// -- HANDOFF: Cycle Launch Agent -> Submission Tracker and Chaser Agent --
// ============================================================
// AGENT 2: SUBMISSION TRACKER AND CHASER AGENT
// ============================================================
// Polling job: every 4 hours while cycle is open
Google Sheets.getRows(forms_response_sheet_id, tab: 'Form Responses') ->
new_rows[]: {
response_timestamp: ISO8601,
respondent_email: string,
employee_id: string, // from pre-filled hidden field
form_type: 'self_assessment' | 'manager_assessment'
}
// Match response to tracker row, update status
Google Sheets.updateRow(tracker_sheet_id) ->
WHERE employee_id matches:
IF form_type == 'self_assessment': self_assessment_status = 'Submitted'
IF form_type == 'manager_assessment': manager_assessment_status = 'Submitted'
last_updated = now()
// Daily reminder job: 09:00 local time
FOR each tracker row WHERE (self_assessment_status == 'Pending' OR manager_assessment_status == 'Pending')
AND reminder_count < max_reminders (default: 3)
AND now() < submission_deadline:
Gmail.send ->
to: employee.email (if self_assessment_status == 'Pending')
subject: 'Reminder: your self-assessment is due [submission_deadline]'
body_vars: { full_name, self_assessment_form_url, submission_deadline, reminder_count }
Slack.post_dm ->
user: employee.slack_id
text: 'Friendly reminder: self-assessment due [submission_deadline].'
Google Sheets.updateRow -> { last_reminder_sent: now(), reminder_count: reminder_count + 1 }
// 48-hour deadline warning (once per outstanding submitter)
FOR each tracker row WHERE status == 'Pending'
AND hours_until_deadline(submission_deadline) <= 48
AND final_warning_sent == false:
Gmail.send -> { subject: 'URGENT: review form due in 48 hours', body_vars: { full_name, submission_deadline } }
Google Sheets.updateRow -> { final_warning_sent: true }
// -- HANDOFF: Submission Tracker Agent -> Summary Drafting Agent --
// Condition: self_assessment_status == 'Submitted' AND manager_assessment_status == 'Submitted'
// ============================================================
// AGENT 3: SUMMARY DRAFTING AGENT
// ============================================================
// Read matched pair of form responses
Google Sheets.getRow(forms_response_sheet_id) ->
self_assessment: {
employee_id, question_1_response, question_2_response,
key_achievements: string, development_areas: string,
overall_rating_self: string
}
manager_assessment: {
employee_id, question_1_response, question_2_response,
strengths_observed: string, areas_for_growth: string,
overall_rating_manager: string, recommended_action: string
}
// AI drafting step
LLM.complete(prompt) ->
input: { employee_name, self_assessment{}, manager_assessment{}, template_sections[] }
output: {
overview: string,
achievements: string,
development_areas: string,
manager_perspective: string,
agreed_next_steps: string
}
// Populate Google Docs template
Google Docs.copy(template_doc_id) -> new_doc_id
Google Docs.batchUpdate(new_doc_id) ->
replace: { '{{EMPLOYEE_NAME}}': employee_name,
'{{CYCLE_NAME}}': cycle_name,
'{{OVERVIEW}}': overview,
'{{ACHIEVEMENTS}}': achievements,
'{{DEVELOPMENT_AREAS}}': development_areas,
'{{MANAGER_PERSPECTIVE}}': manager_perspective,
'{{AGREED_NEXT_STEPS}}': agreed_next_steps }
// Save to manager Drive folder and set permissions
Google Drive.files.update(new_doc_id) -> parents: [manager_drive_folder_id]
Google Drive.permissions.create(new_doc_id) -> { role: 'writer', emailAddress: manager.email }
// Notify manager
Gmail.send ->
to: manager.email
subject: 'Summary ready: [employee_name] — [cycle_name]'
body_vars: { manager_name, employee_name, doc_url, cycle_name }
// Update tracker
Google Sheets.updateRow -> {
summary_status: 'Draft Ready',
summary_doc_url: doc_url,
summary_generated_at: now()
}
// -- HANDOFF: Summary Drafting Agent -> Human gated approval --
// ============================================================
// HUMAN STEP: CALIBRATION MEETING (not automated)
// HR Director and managers conduct calibration; HR sets:
// tracker_sheet.final_rating_approved = true
// tracker_sheet.final_rating_value = agreed_rating
// ============================================================
// Post-approval: BambooHR write-back (gated on final_rating_approved == true)
BambooHR.POST /v1/employees/{employee_id}/performance_reviews ->
payload: {
cycle_name: string,
final_rating: final_rating_value,
review_notes_url: summary_doc_url,
reviewed_by: manager_id,
approved_by: hr_approver_id,
completed_at: now()
}
// ============================================================
// END OF CYCLE
// All scheduled triggers are paused once cycle_status == 'closed'
// ============================================================04Recommended build stack
More documents for this process
Every document generated for Performance Review Cycle.