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
Board and Investor Reporting Automation
[YourCompany.com] · Management Department · Prepared by FullSpec · [Today's Date]
This document gives the FullSpec build team everything needed to configure, connect, and ship the Board and Investor Reporting automation. It covers the current manual process and its bottlenecks, full specifications for all three agents, the end-to-end data flow with exact field names, and the recommended build stack with total time estimates. You will not need to refer to any other document to begin building. Where the process owner must take an action before build can proceed, that requirement is called out explicitly.
01Process snapshot
02Agent specifications
Connects to Xero and HubSpot on a time-based schedule that fires on the configured report date each period. Authenticates via OAuth 2.0 to both platforms, pulls profit and loss, cash flow, and balance sheet data from Xero, and pulls pipeline by stage, weighted forecast, and closed-won revenue from HubSpot. Applies field mapping rules and basic validation checks before writing all structured results to a designated Google Sheets staging tab. Replaces the manual CSV export, copy-paste, and figure-entry steps that currently consume 45 to 70 minutes of CFO time per cycle.
// Input reporting_period: string // e.g. '2024-06' or 'Q2-2024' xero_tenant_id: string // obtained during OAuth flow hubspot_pipeline_id: string // target pipeline in HubSpot target_sheet_id: string // Google Sheets staging document ID target_tab_name: string // e.g. 'DataStaging_June2024' // Output sheet_row.period: string sheet_row.revenue_total: number // from Xero P&L sheet_row.gross_profit: number // from Xero P&L sheet_row.cash_position: number // from Xero balance sheet sheet_row.pipeline_total: number // from HubSpot weighted forecast sheet_row.closed_won_revenue: number sheet_row.deals_by_stage: object // keyed by stage name sheet_row.validation_flags: array // list of any fields outside expected ranges sheet_row.written_at: timestamp
- Xero OAuth 2.0: required scopes are openid, profile, email, accounting.reports.read, and accounting.settings.read. The business must generate a connected app in the Xero developer portal and supply the client ID and secret before build begins. Confirm the correct tenant ID if the account has multiple Xero organisations.
- HubSpot: use a private app token scoped to crm.objects.deals.read and crm.schemas.deals.read. Confirm the target pipeline ID and deal stage IDs match the live HubSpot account before mapping field names.
- Google Sheets: provision a service account in Google Cloud Console, share the staging sheet with the service account email, and store the JSON key securely in the credential store. The staging tab must follow the agreed naming convention (e.g. DataStaging_[Period]) to allow downstream agents to locate it reliably.
- Validation rules: flag any revenue_total or cash_position value that deviates more than 30% from the prior period as a warning in validation_flags. Do not block the workflow; log the flag and continue.
- Dedupe: if the agent fires twice for the same period (e.g. due to a manual re-trigger), check for an existing row with a matching period value in the staging tab. If found, overwrite rather than append to avoid duplicate rows downstream.
- Confirm before build: Xero OAuth token is issued and tested, HubSpot pipeline ID and stage names are confirmed, Google Sheets staging document exists with correct sharing permissions, and the reporting period format is agreed.
Starts immediately after the Data Collector Agent marks its run complete. Sends a structured Slack message to each department head listing the specific KPI fields they are responsible for confirming. Tracks responses against a configurable deadline (default 24 hours). If a department head has not responded within the deadline window, sends a single automated reminder. Once all responses are received, or the final deadline passes, writes confirmed KPI values and any missing flags into the Google Sheets staging tab, appending to the same row as the financial data. This agent replaces the manual chasing process that currently consumes 60 minutes per cycle and introduces response tracking with timestamps.
// Input
reporting_period: string // passed from Data Collector Agent
department_heads: array // [{slack_user_id, name, kpi_fields[]}]
response_deadline_hours: number // default 24
reminder_offset_hours: number // default 18 (reminder fires after this delay)
target_sheet_id: string
target_tab_name: string
// Output (written to Google Sheets staging tab)
kpi_row.dept_name: string
kpi_row.respondent_slack_id: string
kpi_row.kpi_field_name: string
kpi_row.kpi_value: number | string
kpi_row.confirmed_at: timestamp
kpi_row.reminder_sent: boolean
kpi_row.missing: boolean // true if no response by final deadline
// On approval (KPI confirmation)
slack_message_ts: string // thread timestamp for audit trail
sheet_status: string // 'complete' | 'partial' | 'missing'- Slack bot token must have chat:write (to send messages), users:read (to resolve user IDs), and channels:read (to verify channel membership). The bot must be added to any private channels used by department heads before go-live.
- KPI field definitions must be finalised and agreed with all department heads before build. The agent sends structured prompts listing exact field names; ambiguous or disputed metrics will produce inconsistent free-text responses that cannot be parsed reliably.
- Response capture: use Slack's interactive message components (button or short-form modal) rather than free-text replies to enforce a structured numeric or dropdown response. This avoids parsing errors on submission.
- Reminder logic: send exactly one reminder per non-responding department head at the reminder_offset_hours mark. Do not send further automated chasers. Log reminder_sent: true against the relevant row in Google Sheets.
- Missing responses: if a department head does not respond by the final deadline, write missing: true and kpi_value: null to the sheet. Pass a list of missing fields to the Pack Assembly Agent so it can flag those cells in the draft pack rather than leaving them blank.
- Confirm before build: Slack bot is created and installed in the workspace, all department head Slack user IDs are confirmed, KPI field list is finalised and approved by the CFO, and reminder and deadline timings are agreed.
Starts once the KPI Collection Agent marks the Google Sheets staging tab as complete or partial for the reporting period. Reads all financial and KPI data from the staging tab and uses it to populate the approved board pack template stored in Google Drive, updating named cell references, chart data ranges, and table figures throughout the document. Before producing the draft, checks every target field for a corresponding value in the staging data and flags any missing or out-of-range cells with a visible comment inside the document. Exports the completed draft as a PDF, uploads it to the designated board folder in Google Drive, and sends a Slack notification to the CEO with a direct link. After CEO approval is recorded, sends a distribution notification to all board member email addresses. Replaces the formatting, proofing, and distribution steps that currently consume 65 minutes per cycle.
// Input
reporting_period: string
staging_sheet_id: string
staging_tab_name: string
template_doc_id: string // Google Drive ID of the master board pack template
board_folder_id: string // Google Drive folder for final output
ceo_slack_user_id: string
board_member_emails: array // [string]
missing_kpi_fields: array // passed from KPI Collection Agent
// Output
draft_doc_id: string // Google Drive ID of the populated draft
draft_pdf_url: string // shareable link to the exported PDF
flagged_cells: array // [{cell_ref, reason}] for missing/out-of-range values
ceo_notification_ts: string // Slack message timestamp
distribution_sent_at: timestamp // set after CEO marks pack as approved
// On approval
approval_recorded_by: string // CEO Slack user ID
approval_timestamp: timestamp
final_pdf_drive_id: string
board_notified: boolean- Google Drive template: the board pack template must use named ranges or placeholder tokens (e.g. {{revenue_total}}, {{cash_position}}) for every field the agent populates. The template must be locked to prevent structural edits between cycles. If the template changes, named ranges must be updated before the next run.
- Google Drive service account: the service account must have Editor access to the board pack template document and the board folder. Confirm this before build. Do not grant broader Drive access than required.
- Chart population: Google Slides or Docs chart data ranges must be linked to the Google Sheets staging tab rather than embedded static figures. Confirm the template uses linked charts before build begins, as static charts cannot be updated programmatically without reimporting image files.
- Missing field flagging: for each entry in missing_kpi_fields or flagged_cells, insert a comment into the relevant cell or text field in the draft document using the Drive API before exporting the PDF. This ensures the CEO can see exactly which figures need manual review.
- PDF export: use the Google Drive export endpoint (application/pdf MIME type) rather than a third-party conversion service. Store the exported PDF in the board_folder_id with a filename convention of BoardPack_[Period]_Draft.pdf.
- CEO approval signal: the simplest implementation is a Slack interactive button ('Approve for distribution') that sets an approval record in Google Sheets and triggers the distribution step. Do not send distribution emails until the approval timestamp is recorded.
- Confirm before build: the board pack template is finalised and locked, all placeholder tokens or named ranges are in place, the board folder ID is confirmed, and board member email addresses are supplied by the process owner.
03End-to-end data flow
// ── TRIGGER ──────────────────────────────────────────────────────────────
schedule_trigger.fires_at = '2024-06-03T08:00:00Z' // third business day after period close
schedule_trigger.reporting_period = '2024-05'
// ── AGENT 1: Data Collector Agent ────────────────────────────────────────
// Xero API call
GET /api.xero.com/api.xro/2.0/Reports/ProfitAndLoss?fromDate=2024-05-01&toDate=2024-05-31
-> xero.revenue_total, xero.cost_of_goods_sold, xero.gross_profit
-> xero.operating_expenses, xero.net_profit
GET /api.xero.com/api.xro/2.0/Reports/BalanceSheet?date=2024-05-31
-> xero.cash_position, xero.total_assets, xero.total_liabilities
GET /api.xero.com/api.xro/2.0/Reports/CashSummary?fromDate=2024-05-01&toDate=2024-05-31
-> xero.cash_inflow, xero.cash_outflow, xero.net_cash_movement
// HubSpot API call
POST /api.hubapi.com/crm/v3/objects/deals/search
body: { pipeline: hubspot_pipeline_id, closedate_range: '2024-05' }
-> hubspot.deals_by_stage: { prospecting, qualified, proposal, closed_won, closed_lost }
-> hubspot.pipeline_total_weighted: number // sum of (amount * probability)
-> hubspot.closed_won_revenue: number
-> hubspot.deal_count_open: number
// Write to Google Sheets staging tab
sheet.DataStaging_2024-05.row[1].period = '2024-05'
sheet.DataStaging_2024-05.row[1].revenue_total = xero.revenue_total
sheet.DataStaging_2024-05.row[1].gross_profit = xero.gross_profit
sheet.DataStaging_2024-05.row[1].net_profit = xero.net_profit
sheet.DataStaging_2024-05.row[1].cash_position = xero.cash_position
sheet.DataStaging_2024-05.row[1].cash_inflow = xero.cash_inflow
sheet.DataStaging_2024-05.row[1].cash_outflow = xero.cash_outflow
sheet.DataStaging_2024-05.row[1].pipeline_total = hubspot.pipeline_total_weighted
sheet.DataStaging_2024-05.row[1].closed_won_revenue = hubspot.closed_won_revenue
sheet.DataStaging_2024-05.row[1].deals_by_stage = hubspot.deals_by_stage (JSON)
sheet.DataStaging_2024-05.row[1].validation_flags = [] // or [{field, reason}]
sheet.DataStaging_2024-05.row[1].written_at = '2024-06-03T08:14:22Z'
sheet.DataStaging_2024-05.row[1].agent1_status = 'complete'
// ── HANDOFF: Data Collector Agent -> KPI Collection Agent ────────────────
// Trigger: agent1_status == 'complete' for reporting_period '2024-05'
// ── AGENT 2: KPI Collection Agent ────────────────────────────────────────
// Slack prompt sent to each department head
slack.postMessage(
channel: dept_head.slack_user_id,
text: 'Please confirm your KPI figures for May 2024.',
blocks: [interactive_fields for each kpi_field in dept_head.kpi_fields]
)
-> slack_message_ts: '1717401600.000100'
// Reminder (fires at reminder_offset_hours = 18 if no response)
slack.postMessage(
channel: dept_head.slack_user_id,
text: 'Reminder: KPI figures for May 2024 are due in 6 hours.',
thread_ts: slack_message_ts
)
// Response received via Slack interactive callback
slack.callback.action_id = 'kpi_submit'
slack.callback.user.id = dept_head.slack_user_id
slack.callback.values = { kpi_field_name: kpi_value }
// Write confirmed KPI to staging tab
sheet.DataStaging_2024-05.kpi_rows[] = {
dept_name: 'Marketing',
respondent_slack_id: 'U012AB3CD',
kpi_field_name: 'website_leads',
kpi_value: 342,
confirmed_at: '2024-06-03T14:22:00Z',
reminder_sent: true,
missing: false
}
// After all responses or deadline passed
sheet.DataStaging_2024-05.row[1].agent2_status = 'complete' // or 'partial'
sheet.DataStaging_2024-05.row[1].missing_fields = ['ops.support_tickets'] // example
// ── HANDOFF: KPI Collection Agent -> Pack Assembly Agent ─────────────────
// Trigger: agent2_status == 'complete' | 'partial' for reporting_period '2024-05'
// ── AGENT 3: Pack Assembly Agent ─────────────────────────────────────────
// Read full staging row
staging_data = sheet.DataStaging_2024-05.row[1] // all financial + KPI fields
// Copy and populate board pack template in Google Drive
drive.files.copy(template_doc_id) -> draft_doc_id = '1BxiM...Hk2pQ'
drive.docs.replaceAllText(
{{revenue_total}} -> staging_data.revenue_total,
{{gross_profit}} -> staging_data.gross_profit,
{{net_profit}} -> staging_data.net_profit,
{{cash_position}} -> staging_data.cash_position,
{{pipeline_total}} -> staging_data.pipeline_total,
{{closed_won_revenue}} -> staging_data.closed_won_revenue,
// ... all KPI fields from kpi_rows
)
// Flag missing or out-of-range cells
drive.docs.insertComment(
doc_id: draft_doc_id,
cell_ref: '{{ops.support_tickets}}',
comment: 'Missing: no response received from Operations by deadline.'
)
// Export PDF
drive.files.export(draft_doc_id, mimeType='application/pdf')
-> draft_pdf stored in board_folder_id as 'BoardPack_2024-05_Draft.pdf'
-> draft_pdf_url = 'https://drive.google.com/file/d/...'
// Notify CEO via Slack
slack.postMessage(
channel: ceo_slack_user_id,
text: 'Draft board pack for May 2024 is ready for review.',
blocks: [link to draft_pdf_url, Approve button]
)
-> ceo_notification_ts: '1717416000.000200'
// ── HUMAN STEP: CEO Reviews, Adds Commentary, and Approves ───────────────
// CEO clicks Approve in Slack
slack.callback.action_id = 'pack_approve'
slack.callback.user.id = ceo_slack_user_id
-> approval_recorded_by: 'U098ZY7WX'
-> approval_timestamp: '2024-06-04T09:05:00Z'
-> sheet.DataStaging_2024-05.row[1].approved = true
// Export final PDF and distribute
drive.files.export(draft_doc_id, mimeType='application/pdf')
-> final_pdf stored as 'BoardPack_2024-05_Final.pdf' in board_folder_id
-> final_pdf_drive_id = '1CyK9...Rp3mN'
// Notify board members
email.send(
to: board_member_emails[],
subject: 'Board Pack - May 2024',
body: 'Please find the board pack for May 2024 linked below.',
attachment_link: final_pdf_drive_id
)
-> distribution_sent_at: '2024-06-04T09:07:44Z'
-> board_notified: true
// ── END ──────────────────────────────────────────────────────────────────04Recommended build stack
More documents for this process
Every document generated for Board & Investor Reporting.