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
Timesheet Collection and Approval
[YourCompany.com] · HR Department · Prepared by FullSpec · [Today's Date]
This document gives the FullSpec build team everything needed to implement the three-agent Timesheet Collection and Approval automation end to end. It covers the current-state step map with bottleneck identification, full agent specifications with IO contracts, the complete data flow from trigger to confirmation, and the recommended build stack. The process owner and HR team are responsible for confirming employee records, validation thresholds, and approval window timings before build begins. FullSpec handles all agent construction, integration wiring, testing, and deployment.
01Process snapshot
02Agent specifications
Monitors the pay period schedule and, on the close date, reads the master Google Sheet to identify employees who have not submitted. Sends personalised reminders via Gmail and Slack with a direct link to the Google Form submission page. Pulls in form responses, validates that all required fields are present and that hour totals fall within agreed thresholds, and writes clean data into the master sheet. Flags anomalies in a dedicated review column for human inspection before handing off to the Approval Agent.
// Input
pay_period_id: string // e.g. '2025-W06'
pay_period_close_date: date // ISO 8601
employee_list: array // from master sheet: [{employee_id, name, email, slack_handle, manager_id}]
submission_status: array // from master sheet: [{employee_id, submitted: bool}]
form_responses: array // Google Forms API: [{employee_id, hours_regular, hours_overtime, project_codes[], submitted_at}]
validation_thresholds: object // {max_hours_per_day: 12, max_hours_per_week: 60}
// Output
master_sheet_updated: bool // true when all valid rows written
rows_written: array // [{employee_id, hours_regular, hours_overtime, project_codes[], status: 'validated'|'flagged'}]
flagged_entries: array // [{employee_id, flag_reason: string}] — written to Anomalies tab
reminders_sent: array // [{employee_id, channel: 'gmail'|'slack', sent_at: timestamp}]
collection_complete: bool // true when all non-flagged rows validated; triggers Approval Agent- Google Workspace tier must include Workspace Business Starter or above for Gmail API sending limits; confirm the daily sending quota (500 messages/day for standard accounts) is sufficient for the employee count per cycle.
- Google Forms must be the single authorised submission channel before go-live. Employees currently submitting via email attachment or paper must be migrated to the Form before the Collection Agent is activated.
- The master Google Sheet must have a stable, named sheet tab ('Submissions_W{n}') with fixed column positions for employee_id, hours_regular, hours_overtime, project_codes, status, and anomaly_flag. Column positions must be confirmed with the HR team before build.
- Validation thresholds (max daily hours, max weekly hours, required project code fields) must be agreed and documented by the process owner before the validation logic is coded. These will be stored as a config object in the automation platform credential store.
- Reminder messages use personalised templates referencing the employee's first name, the pay period dates, and a unique Google Form prefill URL. Template content must be reviewed and approved by the HR team before build.
- Deduplication: if a Form response arrives more than once for the same employee_id in the same pay_period_id, only the most recent submission is retained. Earlier entries are logged to an Audit tab, not overwritten silently.
- Anomaly flagging does not block the pay period. Flagged rows are written to the master sheet with status 'flagged' and highlighted in the Anomalies tab. The HR administrator resolves flagged entries manually before the Approval Agent is triggered. The Collection Agent will not set collection_complete: true while any unresolved flagged entries remain.
Triggered when the Collection Agent sets collection_complete to true in the master sheet. Reads the master sheet to group validated employee rows by line manager, then sends each manager a pre-formatted approval email listing their team's hours for the pay period. Each email contains a unique token-based approve link and a query link. The agent polls for link activations, logs approval timestamps against each employee row in the master sheet, and escalates automatically if a manager has not responded within the configured window. Escalation sends a follow-up via both Gmail and Slack.
// Input
pay_period_id: string // passed from Collection Agent output
validated_rows: array // [{employee_id, manager_id, hours_regular, hours_overtime, project_codes[]}]
manager_list: array // [{manager_id, name, email, slack_handle}]
approval_window_hours: int // config; e.g. 24 — time before first escalation
escalation_window_hours: int // config; e.g. 4 — time before second escalation
// On approval (webhook callback from token link)
approval_token: string // UUID bound to manager_id + pay_period_id
action: enum // 'approve' | 'query'
responded_at: timestamp
// Output
approval_log: array // [{manager_id, employee_ids[], approved_at: timestamp, action: 'approve'|'query'}]
master_sheet_updated: bool // approval columns populated for each employee row
escalations_sent: array // [{manager_id, channel: 'gmail'|'slack', sent_at: timestamp}]
all_approvals_complete: bool // true when every manager_id has actioned 'approve'; triggers Payroll Sync Agent- Approval tokens are UUIDs generated per manager per pay period. Tokens expire after 72 hours. If a token expires without action, the agent treats it as a non-response and escalates. Token generation and validation must be implemented within the automation platform; no external auth service is required at Standard tier.
- Token-based links are sufficient for Standard build. If the organisation requires formal digital signatures or has strict SSO/access controls, an additional authentication layer must be scoped and agreed before build starts. This is out of scope for the current build specification.
- Each manager receives one approval email per pay period covering all of their direct reports. The email must not be sent until collection_complete is confirmed, preventing partial approval requests.
- If a manager clicks 'query' rather than 'approve', the Approval Agent logs the query action and sends an alert to the HR administrator (via Slack) to resolve the discrepancy manually. The pay period remains open for that manager until an 'approve' action is received.
- The escalation timer is configurable in the credential/config store. Default values are 24 hours for first escalation and 4 hours for second escalation. A third non-response triggers a Slack alert to the HR administrator and payroll owner.
- Master sheet approval columns to be written: approved_at (timestamp), approved_by (manager display name), approval_token_used (UUID), and pay_period_id. Column positions must be confirmed before build.
- Deduplication: if a token is activated more than once (e.g. double-click), only the first activation is logged. Subsequent activations for the same token are discarded and logged to the Audit tab.
Triggered when the Approval Agent sets all_approvals_complete to true. Reads the final approved rows from the master Google Sheet, maps each employee's hours to the corresponding Xero pay item using the employee_xero_id lookup, and pushes the data to Xero via the Payroll API. Verifies the Xero response for each employee record and retries on transient errors. Once all records are confirmed accepted by Xero, sends a Slack confirmation message to the payroll owner summarising the total employee count and total hours processed.
// Input
pay_period_id: string // passed from Approval Agent output
approved_rows: array // [{employee_id, employee_xero_id, hours_regular, hours_overtime, project_codes[], approved_at}]
xero_pay_period_id: string // Xero PayrollCalendar period reference; must match pay_period_close_date
xero_pay_item_map: object // {hours_regular: 'EarningsRateID_001', hours_overtime: 'EarningsRateID_002'}
// Output
xero_sync_results: array // [{employee_xero_id, status: 'accepted'|'rejected', xero_response_code: int}]
sync_complete: bool // true when all records accepted by Xero
rejected_records: array // [{employee_xero_id, reason: string}] — triggers HR alert if non-empty
slack_confirmation_sent: bool
confirmation_message: object // {channel: '#payroll', text: 'Pay period {pay_period_id} synced. {n} employees, {total_hours} hours loaded into Xero.'}- Xero Payroll API access requires an OAuth 2.0 connection with the payroll.read and payroll.write scopes. The Xero app must be registered in the Xero developer portal before build begins. Credentials are stored in the automation platform credential store, not hardcoded.
- Xero employee records must match the master Google Sheet employee list exactly on employee_xero_id. Any discrepancies (missing Xero ID, mismatched name, deactivated employee record) will cause a rejected_record entry and must be reconciled by the HR team before the Payroll Sync Agent is enabled. This reconciliation is a pre-build dependency.
- EarningsRateIDs for regular hours and overtime must be retrieved from the Xero organisation's payroll settings and stored in the pay_item_map config object. These IDs are organisation-specific and cannot be assumed.
- Xero Payroll API rate limit is 60 requests per minute. For up to 80 employees the sync will remain within limits. Batching is not required at this volume but the retry logic must handle 429 responses with exponential backoff (initial delay 2 seconds, maximum 3 retries).
- If any records are rejected by Xero, sync_complete remains false, rejected_records are written to the master sheet Errors tab, and a Slack alert is sent to the HR administrator and payroll owner with the reason string from the Xero response. The sync does not silently fail.
- The Slack confirmation message is sent to the '#payroll' channel (or the channel name confirmed by the process owner) and tags the payroll owner by Slack user ID. Channel name and user ID must be confirmed before build.
- Gusto is referenced in the current-state process but the Standard build target for the API sync is Xero. If the organisation uses Gusto, a separate adapter must be scoped. This specification covers Xero only.
03End-to-end data flow
// ─────────────────────────────────────────────────────────────────
// TRIGGER
// ─────────────────────────────────────────────────────────────────
SCHEDULED_TRIGGER fires on pay_period_close_date
-> emits: { pay_period_id, pay_period_close_date }
// ─────────────────────────────────────────────────────────────────
// COLLECTION AGENT — steps 1 through 5 replaced
// ─────────────────────────────────────────────────────────────────
// Step 1 replacement: read master sheet for submission status
Google_Sheets.read('Master_Timesheets', range='A:H')
-> employee_list[{employee_id, name, email, slack_handle, manager_id}]
-> submission_status[{employee_id, submitted: bool}]
// Step 2 replacement: send personalised reminders to non-submitters
FOR each employee WHERE submitted == false:
Gmail.send({ to: employee.email, template: 'reminder_v1', prefill_url: Forms.prefill(employee_id, pay_period_id) })
Slack.post({ channel: employee.slack_handle, text: reminder_text })
-> reminders_sent[{employee_id, channel, sent_at}]
// Step 3 replacement: collect Google Forms responses
Google_Forms.listResponses(form_id, filter: pay_period_id)
-> form_responses[{employee_id, hours_regular, hours_overtime, project_codes[], submitted_at}]
// Step 4 replacement: validate and write to master sheet
FOR each form_response:
IF duplicate(employee_id, pay_period_id): retain latest, log earlier to Audit tab
IF hours_regular + hours_overtime > validation_thresholds.max_hours_per_week:
status = 'flagged'; write to Anomalies tab
ELSE:
status = 'validated'
Google_Sheets.write('Master_Timesheets', row: { employee_id, hours_regular, hours_overtime, project_codes[], status })
// Step 5 replacement: anomaly review gate
IF flagged_entries.length > 0:
WAIT for HR_Administrator to resolve all flagged rows (manual step)
HR_Administrator updates status = 'validated' in master sheet
WHEN flagged_entries.length == 0:
Google_Sheets.write('Master_Timesheets', cell: collection_complete = true)
-> collection_complete: true
// ─────────────────────────────────────────────────────────────────
// HANDOFF: Collection Agent -> Approval Agent
// Condition: collection_complete == true
// Payload: pay_period_id, validated_rows[], manager_list[]
// ─────────────────────────────────────────────────────────────────
// APPROVAL AGENT — steps 6 through 8 replaced
// ─────────────────────────────────────────────────────────────────
// Step 6 replacement: route approval requests
FOR each manager_id in validated_rows (distinct):
approval_token = UUID.generate(manager_id, pay_period_id)
Gmail.send({ to: manager.email, template: 'approval_request_v1',
team_rows: validated_rows.filter(manager_id),
approve_url: token_endpoint + '?token=' + approval_token + '&action=approve',
query_url: token_endpoint + '?token=' + approval_token + '&action=query' })
// Step 7 replacement: escalation timer
START timer(approval_window_hours) per manager_id
IF no callback received within approval_window_hours:
Gmail.send(escalation_email_v1 -> manager.email)
Slack.post(escalation_message -> manager.slack_handle)
IF no callback within escalation_window_hours after first escalation:
Slack.post(alert -> '#payroll' + HR_Administrator.slack_handle)
// Token webhook callback
ON POST /webhook/approval?token={approval_token}&action={approve|query}:
IF duplicate activation: discard, log to Audit tab
approval_log.push({ manager_id, employee_ids[], approved_at, action })
// Step 8 replacement: write approvals to master sheet
Google_Sheets.write('Master_Timesheets', columns:
{ approved_at, approved_by, approval_token_used, pay_period_id })
-> per employee row, matched on employee_id
WHEN all manager_ids actioned 'approve':
Google_Sheets.write('Master_Timesheets', cell: all_approvals_complete = true)
-> all_approvals_complete: true
// ─────────────────────────────────────────────────────────────────
// HANDOFF: Approval Agent -> Payroll Sync Agent
// Condition: all_approvals_complete == true
// Payload: pay_period_id, approved_rows[], xero_pay_period_id
// ─────────────────────────────────────────────────────────────────
// PAYROLL SYNC AGENT — steps 9 and 10 replaced
// ─────────────────────────────────────────────────────────────────
// Step 9 replacement: push approved hours to Xero
FOR each approved_row:
xero_payload = {
EmployeeID: approved_row.employee_xero_id,
PayPeriod: xero_pay_period_id,
EarningsLines: [
{ EarningsRateID: pay_item_map.hours_regular, NumberOfUnits: approved_row.hours_regular },
{ EarningsRateID: pay_item_map.hours_overtime, NumberOfUnits: approved_row.hours_overtime }
]
}
Xero.PayrollAPI.PUT('/payslip/{EmployeeID}', xero_payload)
ON 429: backoff(2s, max_retries=3)
ON 4xx/5xx: rejected_records.push({ employee_xero_id, reason: response.message })
// Step 10 replacement: verify and confirm
IF rejected_records.length > 0:
Google_Sheets.write('Errors_Tab', rejected_records)
Slack.post({ channel: '#payroll', text: 'Sync partial — {n} rejected records. See Errors tab.' })
ELSE:
sync_complete = true
Slack.post({ channel: '#payroll',
text: 'Pay period {pay_period_id} synced. {approved_rows.length} employees,
{sum(hours_regular + hours_overtime)} total hours loaded into Xero.' })
// ─────────────────────────────────────────────────────────────────
// END: sync_complete == true, payroll owner notified via Slack
// ─────────────────────────────────────────────────────────────────04Recommended build stack
More documents for this process
Every document generated for Timesheet Collection & Approval.