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
Payroll Processing Automation
[YourCompany.com] · Finance Department · Prepared by FullSpec · [Today's Date]
This document is the primary technical reference for the FullSpec team building the Payroll Processing automation. It covers the full current-state process map with bottleneck flags, all three agent specifications with IO contracts, the end-to-end data flow trace, and the recommended build stack. The FullSpec team uses this document to configure, build, and test the automation end to end. Your team's responsibility is to confirm API access, supply accurate leave and deduction rule documentation, and be available during the parallel run review.
01Process snapshot
02Agent specifications
Monitors the configured pay period close date using a scheduled trigger. On trigger, calls the Deputy API to pull all approved shift hours for the closed period. Identifies any employees or cost centres with missing or unapproved timesheets and sends automated reminder messages to the responsible managers via Slack and Gmail. Continues polling the Deputy API at a defined interval until all timesheets are approved or the escalation timeout is reached, at which point it flags unresolved items for human resolution and passes the confirmed data downstream. Complexity: Moderate.
// Input
pay_period_start_date: ISO8601 date string
pay_period_end_date: ISO8601 date string
employee_roster: array of { employee_id, manager_id, deputy_employee_id }
reminder_channels: { slack_channel_id, manager_email }
escalation_timeout_hours: integer (default 24)
// Output (on success)
approved_timesheets: array of {
employee_id: string,
deputy_employee_id: string,
period_hours: float,
overtime_hours: float,
timesheet_status: 'approved' | 'manager_confirmed',
approved_at: ISO8601 timestamp
}
unresolved_flags: array of {
employee_id: string,
reason: 'missing' | 'unapproved' | 'timeout',
escalated_at: ISO8601 timestamp
}
collection_complete: boolean
handoff_timestamp: ISO8601 timestamp- Deputy API access requires the Deputy Premium or Enterprise plan for full Timesheets endpoint availability. Confirm the connected account has the 'Timesheets:Read' OAuth scope before build. The endpoint is GET /api/v1/resource/Timesheet with filters for modified_start and modified_end aligned to the pay period.
- The polling interval for unapproved timesheets should be set to 4 hours by default. The escalation timeout should be configurable via environment variable (TIMESHEET_ESCALATION_HOURS, default 24) rather than hardcoded.
- Slack reminders must be sent to a named channel (e.g. #payroll-alerts) rather than direct messages to individual managers, to preserve an audit thread. The channel name must be confirmed with the process owner before build.
- Gmail reminders use a templated draft. The subject line and body template must be stored as environment variables so the process owner can adjust wording without a code change.
- Dedupe: if a reminder has already been sent to a manager within the same polling cycle, suppress the duplicate. Use a transient in-memory flag keyed on manager_id and reset each polling cycle.
- If collection_complete is false when the downstream agent is triggered, the Reconciliation and Calculation Agent must receive the unresolved_flags array and treat those employees as exceptions, not as missing data errors.
- Confirm before build: pay period close dates, whether the schedule is bi-weekly or monthly, and whether any employees are on a different frequency.
Receives the confirmed timesheet data from the Timesheet Collection Agent. Pulls the published schedule from Deputy for the same period and compares actual approved hours against scheduled hours to identify discrepancies, unclaimed overtime, and leave conflicts. Applies leave and deduction rules sourced from the HR configuration sheet (a designated Google Sheet tab confirmed during data audit). Calculates gross pay per employee using the rate card stored in configuration. Stages corresponding journal entries in QuickBooks via API. Writes the completed payroll summary, including gross pay, deductions, and net pay per employee, to the designated Google Sheets tab. Marks the summary as ready for approval and triggers the downstream agent. Complexity: Complex.
// Input
approved_timesheets: array (from Timesheet Collection Agent output)
unresolved_flags: array (passed through for exception annotation)
pay_period_start_date: ISO8601 date string
pay_period_end_date: ISO8601 date string
config_sheet_id: string (Google Sheets document ID)
config_tab_leave_rules: string (sheet tab name, e.g. 'LeaveRules')
config_tab_rate_card: string (sheet tab name, e.g. 'RateCard')
payroll_summary_sheet_id: string
payroll_summary_tab: string (e.g. 'PayrollSummary_YYYY-MM-DD')
quickbooks_realm_id: string
// Output
payroll_summary: array of {
employee_id: string,
employee_name: string,
scheduled_hours: float,
actual_hours: float,
overtime_hours: float,
leave_hours: float,
leave_type: string,
gross_pay: float,
deductions_total: float,
net_pay: float,
exception_flag: boolean,
exception_reason: string | null
}
summary_sheet_url: string
quickbooks_journal_id: string
total_gross_payroll: float
exception_count: integer
summary_ready: boolean
summary_timestamp: ISO8601 timestamp
// On exception
exception_report: array of {
employee_id: string,
exception_type: 'hours_discrepancy' | 'missing_rate' | 'leave_conflict' | 'unresolved_timesheet',
detail: string
}- QuickBooks API access requires the QuickBooks Online Accounting scope (com.intuit.quickbooks.accounting). OAuth 2.0 credentials must be provisioned and stored in the shared credential store before build. Confirm the connected entity's realm_id.
- The rate card and leave rules must exist in a confirmed, structured Google Sheet tab before the agent can be built. This is the primary dependency flagged in the data audit stage. Do not proceed to agent build until these tabs are validated.
- Gross pay calculation applies the hourly rate from the rate card multiplied by regular hours, plus the overtime multiplier (typically 1.5x) for any hours beyond the employee's contracted weekly threshold. Overtime threshold must be stored as a config field per employee, not hardcoded.
- Leave deductions reduce net pay according to the leave type: unpaid leave deducts at the full hourly rate, sick leave and annual leave are paid at the standard rate per the HR policy. The leave type mapping must be confirmed with the process owner before build.
- Google Sheets writes should use batch update calls rather than row-by-row writes to stay within API rate limits (100 requests per 100 seconds per user).
- QuickBooks journal entry staging uses the JournalEntry endpoint. Debit the payroll expense account and credit the payroll liability account. Chart of accounts mapping must be confirmed with the finance manager before build.
- If any exception_flag is true, a structured exception report must be posted to the #payroll-alerts Slack channel before the summary_ready signal is sent downstream, giving the finance manager an opportunity to intervene before the approval email is generated.
- A new Google Sheets tab must be created per pay period, named using the convention PayrollSummary_YYYY-MM-DD, to maintain a clean audit trail.
Receives the summary_ready signal from the Reconciliation and Calculation Agent. Generates a structured approval request email including the payroll summary totals, a link to the Google Sheets summary tab, and a one-click approval link (implemented as a unique tokenised webhook URL). Sends the email to the business owner via Gmail. Monitors for the approval signal by listening for an inbound webhook call on the unique approval token. On approval, calls the Gusto API to push the finalised payroll data and initiate the payroll run. Posts a confirmation message to the Slack finance channel including the total payroll value and submission timestamp. Does not submit to Gusto without explicit approval. Complexity: Moderate.
// Input
payroll_summary: array (from Reconciliation and Calculation Agent output)
total_gross_payroll: float
summary_sheet_url: string
pay_period_end_date: ISO8601 date string
approval_recipient_email: string (business owner, from environment config)
approval_webhook_base_url: string
gusto_company_id: string
slack_confirmation_channel_id: string
approval_timeout_hours: integer (default 48)
// On approval (webhook inbound)
approval_token: string (UUID, matched against stored token)
approved_by: string (email of approver)
approved_at: ISO8601 timestamp
// Output (on successful submission)
gusto_payroll_id: string
gusto_submission_timestamp: ISO8601 timestamp
gusto_total_submitted: float
slack_confirmation_sent: boolean
run_complete: boolean
// On timeout (no approval within approval_timeout_hours)
escalation_alert: {
channel: slack_confirmation_channel_id,
message: 'Payroll approval pending. No response from approver within timeout window.',
timestamp: ISO8601
}- Gusto API access requires the Gusto Employer API with the payroll:write scope. The company must be on a Gusto plan that includes API access (Gusto Plus or above). Confirm company_id and OAuth credentials before build.
- The approval token must be a UUID generated fresh per payroll cycle and stored in the orchestration layer's credential store with a TTL matching approval_timeout_hours. Expired tokens must return a 403 and post an alert to Slack.
- The approval email must include: the pay period dates, total employee count, total gross payroll amount, a link to the Google Sheets summary, and the one-click approval link. The business owner must be able to make a genuine decision from the email alone without opening the sheet.
- Do not implement an 'auto-approve' fallback. If the approval timeout is reached, escalate via Slack and halt the run. The process owner must re-trigger approval manually.
- Gusto payroll submission uses the POST /v1/companies/{company_id}/payrolls endpoint. Map net pay and deduction fields from the payroll_summary array to Gusto's employee_compensations structure. Confirm field mapping against the Gusto API reference for the connected plan tier.
- The Slack confirmation message must include: the Gusto payroll ID, total gross payroll value, submission timestamp, and a link to the Google Sheets summary tab for the period.
- Gmail sending must use the authenticated Finance Manager's Gmail account (via OAuth 2.0 with the gmail.send scope), not a generic service account, to preserve the email thread context for compliance purposes.
03End-to-end data flow
// ============================================================
// PAYROLL PROCESSING AUTOMATION: END-TO-END DATA FLOW
// ============================================================
// TRIGGER
// Scheduled trigger fires on pay_period_end_date (env var: PAYROLL_SCHEDULE_CRON)
// Fields initialised at trigger:
// pay_period_start_date: '2025-04-15'
// pay_period_end_date: '2025-04-28'
// employee_roster: loaded from config Google Sheet tab 'Roster'
// ============================================================
// AGENT 1: Timesheet Collection Agent
// ============================================================
// Step 1a: Pull approved timesheets from Deputy
// Deputy API: GET /api/v1/resource/Timesheet
// Params: modified_start=pay_period_start_date, modified_end=pay_period_end_date
// OAuth scope required: Timesheets:Read
deputy_response -> parse {
deputy_employee_id, // string, Deputy internal ID
employee_id, // string, mapped from Roster tab
manager_id, // string
start_time, // ISO8601 datetime
end_time, // ISO8601 datetime
total_time, // float, decimal hours
status // 'approved' | 'pending' | 'open'
}
// Step 1b: Identify missing or unapproved timesheets
// Compare deputy_response[].employee_id against employee_roster[].employee_id
// Where status != 'approved': add to unresolved_flags[]
unresolved_flags[] -> {
employee_id: string,
reason: 'missing' | 'unapproved' | 'timeout',
escalated_at: ISO8601 timestamp
}
// Step 1c: Send reminders for unresolved items
// Slack: POST /api/chat.postMessage -> channel: SLACK_PAYROLL_ALERTS_CHANNEL
// Gmail: POST /gmail/v1/users/me/messages/send -> to: manager_email
// Polling interval: TIMESHEET_ESCALATION_HOURS (default 24h)
// Step 1d: Handoff to Agent 2
// Condition: collection_complete = true (all approved) OR escalation_timeout reached
handoff_1_to_2 -> {
approved_timesheets[]: array of approved timesheet records
unresolved_flags[]: array (may be empty)
collection_complete: boolean
handoff_timestamp: ISO8601
}
// ============================================================
// AGENT 2: Reconciliation and Calculation Agent
// ============================================================
// Step 2a: Pull published schedule from Deputy for comparison
// Deputy API: GET /api/v1/resource/Schedule
// Params: start=pay_period_start_date, end=pay_period_end_date
deputy_schedule_response -> parse {
deputy_employee_id,
scheduled_start,
scheduled_end,
scheduled_hours // float
}
// Step 2b: Compare actual vs scheduled hours per employee
// hours_discrepancy = actual_hours - scheduled_hours
// If abs(hours_discrepancy) > DISCREPANCY_THRESHOLD_HOURS (env var, default 1.0):
// exception_flag = true, exception_type = 'hours_discrepancy'
// Step 2c: Load leave and deduction rules from Google Sheets
// Google Sheets API: GET spreadsheets/{config_sheet_id}/values/{config_tab_leave_rules}
// Google Sheets API: GET spreadsheets/{config_sheet_id}/values/{config_tab_rate_card}
leave_rules -> {
employee_id,
leave_type, // 'annual' | 'sick' | 'unpaid' | 'public_holiday'
leave_hours,
leave_balance_remaining
}
rate_card -> {
employee_id,
hourly_rate, // float, USD
overtime_threshold, // float, weekly hours before OT kicks in
overtime_multiplier // float, default 1.5
}
// Step 2d: Calculate gross pay per employee
// regular_hours = min(actual_hours, overtime_threshold)
// overtime_hours = max(0, actual_hours - overtime_threshold)
// gross_pay = (regular_hours * hourly_rate) + (overtime_hours * hourly_rate * overtime_multiplier)
// deductions = sum of applicable deductions from leave_rules and config
// net_pay = gross_pay - deductions_total
// Step 2e: Write payroll summary to Google Sheets
// Google Sheets API: POST spreadsheets/{payroll_summary_sheet_id}/values:batchUpdate
// Tab name: PayrollSummary_{pay_period_end_date}
summary_row -> {
employee_id, employee_name,
scheduled_hours, actual_hours, overtime_hours, leave_hours, leave_type,
gross_pay, deductions_total, net_pay,
exception_flag, exception_reason
}
// Step 2f: Stage journal entry in QuickBooks
// QuickBooks API: POST /v3/company/{quickbooks_realm_id}/journalentry
// OAuth scope: com.intuit.quickbooks.accounting
journal_entry -> {
Line[0]: { DetailType: 'JournalEntryLineDetail', Amount: total_gross_payroll,
JournalEntryLineDetail: { PostingType: 'Debit', AccountRef: payroll_expense_account } }
Line[1]: { DetailType: 'JournalEntryLineDetail', Amount: total_gross_payroll,
JournalEntryLineDetail: { PostingType: 'Credit', AccountRef: payroll_liability_account } }
DocNumber: 'PAY_{pay_period_end_date}'
}
// Step 2g: Post exception report to Slack if exception_count > 0
// Slack: POST /api/chat.postMessage -> channel: SLACK_PAYROLL_ALERTS_CHANNEL
// Step 2h: Handoff to Agent 3
handoff_2_to_3 -> {
payroll_summary[]: array of summary_row objects
total_gross_payroll: float
summary_sheet_url: string
quickbooks_journal_id: string
exception_count: integer
summary_ready: boolean
summary_timestamp: ISO8601
}
// ============================================================
// AGENT 3: Approval and Submission Agent
// ============================================================
// Step 3a: Generate unique approval token
// approval_token = UUID v4, stored with TTL = APPROVAL_TIMEOUT_HOURS (default 48h)
// approval_webhook_url = '{APPROVAL_WEBHOOK_BASE_URL}/approve?token={approval_token}'
// Step 3b: Send approval request email via Gmail
// Gmail API: POST /gmail/v1/users/me/messages/send
// OAuth scope: gmail.send
// To: APPROVER_EMAIL (env var)
// Payload includes:
// pay_period_start_date, pay_period_end_date,
// employee_count (len(payroll_summary)),
// total_gross_payroll,
// summary_sheet_url,
// approval_webhook_url
// Step 3c: Wait for approval webhook
// Inbound: GET/POST {APPROVAL_WEBHOOK_BASE_URL}/approve?token={approval_token}
// Validate: approval_token matches stored token AND token not expired
// On valid: record approved_by, approved_at; proceed to Step 3d
// On expired token: return 403, post Slack escalation, halt
// On timeout (APPROVAL_TIMEOUT_HOURS exceeded): post Slack escalation, halt
// Step 3d: Push approved payroll to Gusto
// Gusto API: POST /v1/companies/{gusto_company_id}/payrolls
// OAuth scope: payroll:write
gusto_payload -> {
pay_period: { start_date, end_date },
employee_compensations[]: {
employee_id: string,
fixed_compensations: [{ name: 'Salary', amount: net_pay }],
hourly_compensations: [{ name: 'Regular', hours: regular_hours },
{ name: 'Overtime', hours: overtime_hours }]
}
}
gusto_response -> parse {
gusto_payroll_id,
gusto_submission_timestamp,
gusto_total_submitted
}
// Step 3e: Post Slack confirmation
// Slack: POST /api/chat.postMessage -> channel: SLACK_CONFIRMATION_CHANNEL
// Payload: gusto_payroll_id, gusto_total_submitted, gusto_submission_timestamp,
// summary_sheet_url, approved_by, approved_at
// ============================================================
// RUN COMPLETE
// run_complete = true
// All outputs written. No further automation steps.
// ============================================================04Recommended build stack
More documents for this process
Every document generated for Payroll Processing.