Back to Commission & Payout Calculation

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

Commission and Payout Calculation

[YourCompany.com] · Finance Department · Prepared by FullSpec · [Today's Date]

This document is the primary technical reference for the FullSpec team building the Commission and Payout Calculation automation. It covers the current-state process map with bottleneck analysis, full agent specifications with IO contracts, the end-to-end data flow, and the recommended build stack. The FullSpec team owns everything described here from environment setup through to go-live. Your team's responsibilities are limited to providing API credentials, confirming rate table structure, and approving outputs at the parallel-run stage.

01Process snapshot

Step
Name
Description
1
Export Closed Deals from CRM
Finance pulls a filtered export of all closed-won deals for the pay period from HubSpot. Downloaded as CSV and saved to a shared folder. Time cost: 20 min.
2
Clean and Format Deal Data
Column headers renamed, currency fields reformatted, duplicate and test records removed manually. Varies widely with CRM export quality. Time cost: 35 min.
3
Look Up Commission Rate for Each Rep
Each rep's quota attainment is checked against the rate table and the applicable tier percentage is noted next to their deals. Rate tables must be cross-referenced manually from a separate tab. Time cost: 45 min.
4
Calculate Commission Amounts
Formulas applied deal by deal to multiply deal value by commission rate, accounting for splits, adjustments, and clawbacks from the prior period. Time cost: 40 min.
5
Compile Rep-Level Payout Summary
Individual deal rows aggregated into per-rep totals. Summary tab built showing rep name, deal count, total revenue, and gross commission. Time cost: 25 min.
6
Manager Review and Sign-Off
Payout summary emailed to sales manager. Manager checks figures and either approves or flags discrepancies. Time cost: 30 min.
7
Handle Disputes and Corrections
Any flagged discrepancy requires the analyst to trace back to the original deal record, confirm data, and recalculate. Can repeat multiple times per cycle. Time cost: 50 min.
8
Enter Approved Payouts into Payroll
Approved commission totals manually keyed into Gusto as additional earnings per rep, switching between summary sheet and payroll one rep at a time. Time cost: 35 min.
9
Record Commission Expense in Accounting
Journal entry created in Xero to record total commission expense against the correct cost centre for the period. Time cost: 20 min.
10
Notify Reps of Their Payout
Each rep sent a breakdown of their commission via Slack so they can verify the amount before the payslip arrives. Time cost: 20 min.
Time cost summary: Total manual time per cycle is 320 minutes (approximately 5 hours 20 minutes). At a bi-weekly cadence this equals approximately 7.5 hours per week and 375 hours per year. Steps 1 and 2 are replaced by the Deal Ingestion Agent. Steps 3, 4, and 5 are replaced by the Commission Calculation Agent. Steps 8, 9, and 10 are replaced by the Payout Distribution Agent. Steps 6 and 7 are retained as human checkpoints: the manager approval gate (step 6) is restructured as a Slack interaction, and dispute handling (step 7) is reduced to reviewing a pre-built exceptions list rather than manual tracing.
Developer Handover PackPage 1 of 4
FS-DOC-04Technical

02Agent specifications

Deal Ingestion Agent

Connects to HubSpot on a scheduled trigger aligned to the configured pay period end date. Calls the HubSpot Deals API to retrieve all deals where stage equals closed-won and close date falls within the pay period window. Normalises field names, reformats currency values to two decimal places, removes records tagged as test or voided, and writes one clean row per deal into a designated Google Sheets tab. All records are tagged with the rep's HubSpot owner ID and a period label derived from the trigger timestamp. No manual intervention is required unless the HubSpot connection returns an authentication error.

Trigger
Scheduled: pay period end date reached, as configured in the orchestration layer
Tools
HubSpot (Deals API), Google Sheets (Sheets API v4)
Replaces steps
Steps 1 and 2: Export Closed Deals from CRM, Clean and Format Deal Data
Estimated build
16 hours, Moderate complexity
// Input
trigger.pay_period_start_date  : ISO 8601 date string
trigger.pay_period_end_date    : ISO 8601 date string
trigger.period_label           : string  e.g. '2024-06-B'

// HubSpot query filter applied
hubspot.deals.filter.stage     = 'closedwon'
hubspot.deals.filter.closedate >= pay_period_start_date
hubspot.deals.filter.closedate <= pay_period_end_date

// Output (one row per deal written to Google Sheets tab: raw_deals_{period_label})
deal_id          : string   (HubSpot deal ID)
deal_name        : string
close_date       : ISO 8601 date string
deal_value       : float    (USD, 2 decimal places)
owner_id         : string   (HubSpot owner ID)
owner_email      : string
product_line     : string   (mapped from hs_deal_stage_probability custom property)
is_split_deal    : boolean
split_percentage : float    (null if is_split_deal = false)
clawback_ref     : string   (deal_id of prior period deal if clawback applies, else null)
period_label     : string
record_status    : string   ('clean' | 'flagged_test' | 'flagged_void')
  • HubSpot tier must be Sales Hub Starter or above; the Deals API endpoint /crm/v3/objects/deals is not available on the free tier. Confirm the account tier before build starts.
  • Google Sheets OAuth scope required: https://www.googleapis.com/auth/spreadsheets. Use a dedicated service account, not a personal Google account, so credential rotation does not break the workflow.
  • The output sheet tab must follow the naming convention raw_deals_{period_label} exactly. Downstream agents reference this tab name dynamically; any deviation will cause a lookup failure.
  • Dedupe logic: if a deal_id already exists in the target sheet for the same period_label, skip the record and log a warning rather than writing a duplicate row.
  • Records where deal_value is zero or null must be flagged as record_status = 'flagged_void' and written to a separate exceptions tab rather than the main deal list.
  • Confirm with the process owner which HubSpot custom property holds product line data before build. The field name varies by HubSpot configuration.
  • The Google Sheets credential must have Editor access on the specific spreadsheet ID used for the payout ledger. Confirm the spreadsheet ID during discovery.
Commission Calculation Agent

Reads the clean deal list written by the Deal Ingestion Agent and the active commission rate table from a separate Google Sheets tab. For each rep, it sums deal values for the period, determines quota attainment percentage, resolves the correct tier from the rate table, and applies the rate to each deal to produce a gross commission line. Split deals are handled by multiplying deal value by the split percentage before applying the rate. Clawback references trigger a negative adjustment line linked to the originating deal ID. Any deal that cannot be matched to a rep record or cannot be resolved to a single rate tier is written to an exceptions list rather than the payout summary. The agent writes its outputs to a payout summary tab and an exceptions tab, then signals the orchestration layer that it has completed.

Trigger
Deal Ingestion Agent completes successfully and raw_deals_{period_label} tab is confirmed non-empty in Google Sheets
Tools
Google Sheets (Sheets API v4, read and write)
Replaces steps
Steps 3, 4, and 5: Look Up Commission Rate, Calculate Commission Amounts, Compile Rep-Level Payout Summary
Estimated build
20 hours, Complex complexity
// Input (read from Google Sheets)
raw_deals_{period_label}          : sheet tab   (output of Deal Ingestion Agent)
commission_rate_table             : sheet tab   (columns: tier_label, quota_attainment_min_pct,
                                                 quota_attainment_max_pct, commission_rate_pct,
                                                 product_line, effective_date)
rep_quota_table                   : sheet tab   (columns: owner_id, owner_email, quota_usd, period_label)

// Processing sequence
1. Group deals by owner_id
2. Sum deal_value per owner_id -> rep_total_revenue
3. rep_quota_attainment_pct = (rep_total_revenue / quota_usd) * 100
4. Match quota_attainment_pct to rate table tier -> resolved_rate_pct
5. For each deal: gross_commission = deal_value * (split_percentage ?? 1.0) * resolved_rate_pct
6. Apply clawback: if clawback_ref != null, append negative line = -(prior_deal_value * resolved_rate_pct)
7. Flag unresolved records -> exceptions list

// Output A: payout_summary_{period_label} tab (one row per rep)
owner_id              : string
owner_email           : string
period_label          : string
deal_count            : integer
rep_total_revenue     : float
quota_usd             : float
quota_attainment_pct  : float
resolved_rate_pct     : float
gross_commission      : float
clawback_adjustment   : float  (negative, 0.00 if none)
net_commission        : float  (gross_commission + clawback_adjustment)
calculation_status    : string ('confirmed' | 'pending_exception')

// Output B: exceptions_{period_label} tab (one row per unresolved deal)
deal_id               : string
owner_id              : string
exception_reason      : string  ('no_rep_match' | 'no_rate_tier' | 'ambiguous_split')
requires_manual_review: boolean  (always true)
  • The rate table schema must be machine-readable before build starts. Merged cells, free-form formatting, or non-numeric tier thresholds will break tier resolution. FullSpec will flag this during discovery.
  • The rep_quota_table tab must include a period_label column so the agent can isolate quota figures for the correct pay period. If historical quota data is not structured this way, a one-off migration is required.
  • If a rep has zero deals in the period, they must still appear in payout_summary with net_commission = 0.00 and calculation_status = 'confirmed'. Do not omit zero-commission reps from the output.
  • Tier boundary handling: if quota_attainment_pct exactly equals a tier boundary value, resolve to the higher tier. Document this rule in the runbook so the manager is aware.
  • Clawback logic depends on prior-period deal records being available in the same spreadsheet. Confirm with the process owner how far back clawbacks can reach; default to one prior period unless instructed otherwise.
  • Product-line-specific rate overrides must be captured as separate rows in the rate table rather than as formulas or conditional formatting. Confirm this with the process owner before building the lookup.
  • Google Sheets API quota: 100 read requests per 100 seconds per user. For periods with more than 120 deal records, implement batch reads to avoid rate limit errors.
Developer Handover PackPage 2 of 4
FS-DOC-04Technical
Payout Distribution Agent

Takes the approved payout summary and executes all downstream distribution steps in sequence. First, it posts a structured Slack message to the sales manager containing the rep-level summary table and an approve or flag interactive button. The workflow pauses at this point and waits for the manager's response. On approval, it pushes each rep's net_commission as an additional earnings line to Gusto using the Gusto Payroll API, creates a single commission expense journal entry in Xero, and sends each rep a personalised Slack DM listing their deal count, rate applied, and net commission for the period. If the manager clicks flag, the workflow routes back to the exceptions handling path and sends an alert to the finance analyst. A configurable timeout (default 48 hours) prevents the workflow from stalling indefinitely if the manager does not respond.

Trigger
Commission Calculation Agent completes and payout_summary_{period_label} tab is confirmed present in Google Sheets with at least one row where calculation_status = 'confirmed'
Tools
Slack (Web API, chat.postMessage, interactive messages), Gusto (Payroll API v1), Xero (Accounting API v2)
Replaces steps
Steps 8, 9, and 10: Enter Approved Payouts into Gusto, Record Commission Expense in Xero, Notify Reps via Slack
Estimated build
18 hours, Complex complexity
// Input (read from Google Sheets)
payout_summary_{period_label}  : sheet tab   (output of Commission Calculation Agent)
exceptions_{period_label}      : sheet tab   (surfaced in Slack approval message if non-empty)

// Step 1: Slack approval message (sent to manager Slack user ID)
slack.channel      : manager_slack_user_id  (configured credential)
slack.blocks       : summary table + approve button + flag button
slack.callback_id  : 'commission_approval_{period_label}'

// On approval (manager clicks Approve)
gusto_payload per rep:
  employee_id       : string   (mapped from owner_email -> Gusto employee lookup)
  earning_type      : 'Commission'
  amount            : net_commission  (float, USD)
  pay_period_label  : period_label

xero_journal_entry:
  date              : trigger.pay_period_end_date
  description       : 'Sales commission - {period_label}'
  line_items[0].account_code  : '420'  (confirm cost centre with process owner)
  line_items[0].amount        : SUM(net_commission) across all reps
  line_items[0].description   : 'Gross sales commission {period_label}'
  attachments[0]              : Google Sheets export of payout_summary_{period_label} (PDF)

// Slack DM per rep (sent after Gusto and Xero steps confirm success)
slack.channel      : rep_slack_user_id  (mapped from owner_email -> Slack user lookup)
slack.text         : 'Hi {first_name}, your commission for {period_label} has been approved.'
slack.blocks       : deal_count, rep_total_revenue, resolved_rate_pct, net_commission

// On flag (manager clicks Flag)
slack.channel      : finance_analyst_slack_user_id
slack.text         : 'Commission approval flagged by manager for {period_label}. Review required.'
workflow.status    : paused_pending_review

// On timeout (48 hours elapsed, no response)
slack.channel      : finance_analyst_slack_user_id
slack.text         : 'Commission approval timed out for {period_label}. Manual action required.'
workflow.status    : timed_out
  • Gusto API access requires the payroll:write scope under an OAuth 2.0 application. The Gusto sandbox environment must be used for all testing before production credentials are issued. Confirm that the account plan permits API access: Gusto Plus or above is required.
  • Employee mapping between HubSpot owner_email and Gusto employee_id must be resolved before the Gusto push step. Build a static mapping table in Google Sheets (columns: owner_email, gusto_employee_id) and confirm it is accurate before go-live.
  • Xero journal entries require the Xero account code for the commission expense cost centre. Default is 420 (Sales and Marketing expenses) but this must be confirmed with the bookkeeper. The Xero API requires offline_access scope for non-interactive token refresh.
  • The Slack interactive message approval requires a Slack app with the chat:write and commands scopes, and an incoming webhook URL registered for the manager's channel. The callback payload from the interactive button must be verified against the Slack signing secret.
  • The approval timeout of 48 hours is configurable at build time. If the pay period close is on a Friday, consider extending to 72 hours to account for the weekend. Document the chosen value in the runbook.
  • If a Gusto push fails for a specific employee (e.g. employee_id not found), the workflow must not abort the entire batch. Log the failure, continue with remaining reps, and surface the failed entries in a Slack alert to the finance analyst.
  • Confirm Xero has the correct base currency set to USD before build. Multi-currency Xero accounts require an additional header in the journal entry payload.
Build time summary across all three agents: Deal Ingestion Agent 16 hours, Commission Calculation Agent 20 hours, Payout Distribution Agent 18 hours. Total agent build hours: 54. End-to-end integration and testing allowance: 10 hours. Total build effort: 64 hours, consistent with the scoped estimate.
Developer Handover PackPage 3 of 4
FS-DOC-04Technical

03End-to-end data flow

Full trigger-to-output data flow with field names at each agent handoff. All credential references resolve from the shared credential store at runtime.
// ============================================================
// COMMISSION AND PAYOUT CALCULATION: END-TO-END DATA FLOW
// ============================================================

// TRIGGER
// Orchestration layer scheduled job fires on pay_period_end_date
trigger.pay_period_start_date  = '2024-06-01'   // ISO 8601
trigger.pay_period_end_date    = '2024-06-15'   // ISO 8601
trigger.period_label           = '2024-06-B'    // derived from end date
trigger.run_id                 = 'run_20240615_001'  // unique per execution

// ============================================================
// AGENT 1: DEAL INGESTION AGENT
// ============================================================

// Step 1: HubSpot Deals API call
GET /crm/v3/objects/deals
  ?filters[0].propertyName=dealstage
  &filters[0].operator=EQ
  &filters[0].value=closedwon
  &filters[1].propertyName=closedate
  &filters[1].operator=BETWEEN
  &filters[1].value=trigger.pay_period_start_date
  &filters[1].highValue=trigger.pay_period_end_date
  &properties=dealname,amount,closedate,hubspot_owner_id,
              hs_product_line,is_split_deal,split_pct,clawback_ref

// Step 2: Normalisation and filtering
for each deal in hubspot_response.results:
  deal_id          = deal.id
  deal_name        = deal.properties.dealname
  close_date       = deal.properties.closedate  // ISO 8601
  deal_value       = float(deal.properties.amount)  // USD, 2dp
  owner_id         = deal.properties.hubspot_owner_id
  owner_email      = hubspot_owners_lookup[owner_id].email
  product_line     = deal.properties.hs_product_line ?? 'standard'
  is_split_deal    = bool(deal.properties.is_split_deal)
  split_percentage = float(deal.properties.split_pct) ?? null
  clawback_ref     = deal.properties.clawback_ref ?? null
  period_label     = trigger.period_label
  record_status    = classify_record(deal_value, deal_name)
    // 'flagged_void' if deal_value == 0 or null
    // 'flagged_test' if deal_name contains '[TEST]'
    // 'clean' otherwise

// Step 3: Write to Google Sheets
// Clean records -> tab: raw_deals_2024-06-B
// Flagged records -> tab: exceptions_2024-06-B
sheets.spreadsheetId  = PAYOUT_LEDGER_SPREADSHEET_ID  // from credential store
sheets.range          = 'raw_deals_2024-06-B!A1'
sheets.valueInputOption = 'USER_ENTERED'
sheets.values         = [deal_id, deal_name, close_date, deal_value,
                          owner_id, owner_email, product_line,
                          is_split_deal, split_percentage, clawback_ref,
                          period_label, record_status]

// Agent 1 completion signal -> triggers Agent 2
agent1.status  = 'complete'
agent1.output  = { tab: 'raw_deals_2024-06-B', row_count: N, flagged_count: M }

// ============================================================
// AGENT 2: COMMISSION CALCULATION AGENT
// ============================================================

// Step 4: Read inputs from Google Sheets
deals       = sheets.read('raw_deals_2024-06-B')           // from Agent 1
rate_table  = sheets.read('commission_rate_table')         // maintained by finance team
quota_table = sheets.read('rep_quota_table')               // filtered by period_label

// Step 5: Per-rep calculation loop
for each owner_id in unique(deals.owner_id):
  rep_deals             = filter(deals, owner_id == owner_id, record_status == 'clean')
  rep_total_revenue     = sum(rep_deals.deal_value)  // adjusted for split_percentage
  quota_usd             = quota_table[owner_id, period_label].quota_usd
  quota_attainment_pct  = (rep_total_revenue / quota_usd) * 100
  resolved_rate_pct     = rate_table.lookup(
                            quota_attainment_pct,
                            product_line,
                            effective_date <= trigger.pay_period_end_date
                          )  // returns null if no match -> exception

  for each deal in rep_deals:
    effective_value    = deal.deal_value * (deal.split_percentage ?? 1.0)
    gross_commission   = effective_value * resolved_rate_pct

  clawback_adjustment  = sum(clawback_lines)  // negative float or 0.00
  net_commission       = sum(gross_commission) + clawback_adjustment

// Step 6: Write outputs to Google Sheets
// Confirmed reps  -> tab: payout_summary_2024-06-B
// Exception reps  -> tab: exceptions_2024-06-B  (appended)
payout_summary.fields = [owner_id, owner_email, period_label, deal_count,
                          rep_total_revenue, quota_usd, quota_attainment_pct,
                          resolved_rate_pct, gross_commission,
                          clawback_adjustment, net_commission, calculation_status]

// Agent 2 completion signal -> triggers Agent 3
agent2.status  = 'complete'
agent2.output  = { tab: 'payout_summary_2024-06-B', confirmed: X, exceptions: Y }

// ============================================================
// AGENT 3: PAYOUT DISTRIBUTION AGENT
// ============================================================

// Step 7: Post Slack approval message to manager
slack.postMessage(
  channel    = MANAGER_SLACK_USER_ID,   // from credential store
  blocks     = build_approval_blocks(payout_summary, exceptions),
  callback_id = 'commission_approval_2024-06-B'
)
workflow.wait_for_interaction(timeout = '48h')

// Step 8a: On manager approval
if interaction.action == 'approve':

  // Push to Gusto
  for each rep in payout_summary where calculation_status == 'confirmed':
    gusto_employee_id = employee_map[rep.owner_email]  // from mapping table
    POST /v1/companies/{company_id}/employees/{gusto_employee_id}/additional_earnings
    body = {
      earning_type : 'Commission',
      amount       : rep.net_commission,
      payment_date : trigger.pay_period_end_date
    }

  // Create Xero journal entry
  POST /api.xro/2.0/ManualJournals
  body = {
    Date        : trigger.pay_period_end_date,
    Narration   : 'Sales commission 2024-06-B',
    JournalLines: [
      { AccountCode: '420', Description: 'Gross commission',
        LineAmount: SUM(payout_summary.net_commission) },
      { AccountCode: '200', Description: 'Commission payable clearing',
        LineAmount: -SUM(payout_summary.net_commission) }
    ]
  }

  // Send Slack DM per rep
  for each rep in payout_summary where calculation_status == 'confirmed':
    rep_slack_id = slack_user_map[rep.owner_email]  // from mapping table
    slack.postMessage(
      channel = rep_slack_id,
      text    = 'Hi {first_name}, your commission for 2024-06-B is {net_commission}.',
      blocks  = build_rep_breakdown_blocks(rep)
    )

// Step 8b: On manager flag or timeout
else:
  slack.postMessage(
    channel = FINANCE_ANALYST_SLACK_USER_ID,
    text    = 'Commission approval flagged or timed out for 2024-06-B. Manual action required.'
  )
  workflow.status = 'paused_pending_review'

// ============================================================
// END OF FLOW
// ============================================================

04Recommended build stack

Orchestration layer
A workflow automation tool with native HTTP request nodes, scheduled triggers, interactive Slack webhook handling, and a shared encrypted credential store. One workflow file per agent (Deal Ingestion, Commission Calculation, Payout Distribution) plus a master orchestration workflow that chains them in sequence and handles error routing. All credentials (HubSpot API key, Google Sheets service account JSON, Slack bot token, Gusto OAuth token, Xero OAuth token) are stored in the platform's credential store and never hard-coded in workflow logic.
Webhook configuration
Two inbound webhooks required. First: a Slack interactive-message webhook to receive the manager's approve or flag callback from the commission approval message (POST, path /webhooks/commission-approval). Second: a test trigger webhook for manually initiating a pay period run during UAT (POST, path /webhooks/commission-test-trigger, disabled in production). Both endpoints must validate the Slack signing secret or a shared HMAC token respectively before processing.
Templating approach
Slack block kit JSON templates are maintained as reusable node-level templates within the orchestration layer: one template for the manager approval message (approval_block_template.json) and one for the per-rep payout DM (rep_breakdown_template.json). Period label, rep name, deal count, rate, and net commission are injected at runtime via string interpolation. Google Sheets tab names follow the convention {sheet_type}_{period_label} throughout so no hardcoded tab names appear in workflow logic.
Error logging
All agent errors are written to a Supabase table (table: commission_automation_errors, columns: run_id, agent_name, step_name, error_code, error_message, payload_snapshot, created_at). On any error write, a Slack alert is sent automatically to the finance analyst's Slack user ID with the run_id, agent name, and a plain-English description of the failure. The Supabase table is retained for a minimum of 12 months to satisfy audit requirements. Errors are classified as warning (non-blocking, logged and continued) or fatal (workflow halted, analyst alerted immediately).
Testing approach
All three agents are built and validated against sandbox environments first: HubSpot sandbox account, Gusto demo company, Xero demo company, and a dedicated test Google Sheets spreadsheet. Slack testing uses a private test workspace channel. A parallel run is conducted against the most recent completed pay period using live data, with analyst cross-checking automated outputs against the prior manual calculation. Go-live is conditional on zero material discrepancies in the parallel run. Regression tests are re-run after any rate table change or credential rotation.
Estimated total build time
Deal Ingestion Agent: 16 hours. Commission Calculation Agent: 20 hours. Payout Distribution Agent: 18 hours. End-to-end integration testing, parallel run support, and error logging setup: 10 hours. Total: 64 hours, matching the scoped build effort. At the Standard build tier, this is delivered across a four to five week window as documented in the delivery schedule.
Before build can start, the FullSpec team requires: HubSpot API key with crm.objects.deals.read scope, Google Sheets service account JSON with Editor access on the payout ledger spreadsheet, Slack bot token with chat:write and the interactive-messages webhook URL registered, Gusto OAuth client ID and secret for the demo company, and Xero OAuth client ID and secret with accounting and offline_access scopes. Any missing credential delays the relevant agent build. Contact support@gofullspec.com to coordinate credential handover.
Developer Handover PackPage 4 of 4

More documents for this process

Every document generated for Commission & Payout Calculation.

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