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
End of Month Close
[YourCompany.com] · Finance Department · Prepared by FullSpec · [Today's Date]
This document is the authoritative technical reference for the End of Month Close automation build. It covers the current-state process map with bottleneck identification, full agent specifications with IO contracts, an end-to-end data flow trace, and the recommended build stack. The FullSpec team uses this document to guide all build decisions. You and your finance team do not need to act on anything in this document directly, but the pre-build confirmation items in each agent specification require sign-off from a process owner before work begins.
01Process snapshot
02Agent specifications
Sends templated receipt and expense chase messages on the last business day of each month to all relevant staff via Gmail and Slack. The agent monitors for replies and send statuses, then fires a 24-hour reminder to any recipient who has not responded. It writes a delivery and reply log to a Google Sheet row for audit purposes. This agent replaces the manual email composition and follow-up work currently done by the bookkeeper at the start of every close cycle. Estimated build: 8 hours. Complexity: Simple.
// Input
recipient_list: [] // array of {name, email, slack_user_id, department}
period_label: string // e.g. 'May 2025'
chase_template_id: string // references Gmail draft template by label
reminder_delay_hours: 24
// Output
delivery_log: [] // array of {recipient_email, sent_at, channel: 'gmail'|'slack', status: 'sent'|'failed'}
reply_received: [] // array of {recipient_email, replied_at, thread_id}
reminder_queue: [] // recipients with no reply after reminder_delay_hours
// On reminder trigger (24h no reply)
reminder_sent: [] // array of {recipient_email, reminder_sent_at, channel}- Gmail API tier required: Google Workspace Business Starter or above. The service account or OAuth client must have the Gmail send scope (https://www.googleapis.com/auth/gmail.send) and readonly scope (https://www.googleapis.com/auth/gmail.readonly) for reply detection. Confirm the connected account before build.
- Slack: use a Bot token with chat:write and channels:read scopes. Do not use legacy incoming webhooks if the Slack workspace is on a paid plan, as channel targeting is more reliable via Bot token.
- Recipient list must be maintained in a named Google Sheet tab ('chase_recipients') with columns: name, email, slack_user_id, department. The agent reads this at runtime; it does not hard-code contacts.
- Gmail templates are stored as draft emails with a specific label prefix ('FullSpec/chase'). The agent clones the draft, substitutes {period_label} and {recipient_name} tokens, and sends. Do not use the Drafts API send endpoint for bulk sending; use the Messages.send endpoint per recipient.
- Dedupe: if the schedule trigger fires more than once in a day (e.g. due to a platform retry), check the delivery_log sheet for an existing entry with the same period_label before sending. Skip if a sent record exists.
- Confirm with the process owner: how many recipients are on the chase list, and whether any departments use a shared inbox that would confuse reply detection.
- Fallback: if a Gmail send fails (API error or quota), log the failure to the delivery_log with status 'failed' and post a Slack alert to the finance channel naming the failed recipient.
Pulls all uncategorised and unreconciled transactions from Xero for the closed period, matches bank feed entries against recorded transactions using rule-based logic, and applies existing Xero coding rules to confident matches. Items that fall below the confidence threshold or match no known rule are written to a structured exceptions tab in Google Sheets for bookkeeper review. The agent also ingests supplier documents from Hubdoc and Dext, linking document references to the matched Xero transaction where possible. This agent replaces Steps 2, 3, 4, and 5. Step 5 reconciliation confirmation is automated for matched items; unmatched items remain in the exceptions list. Estimated build: 22 hours. Complexity: Complex.
// Input
xero_tenant_id: string
period_start: date // first day of closed month
period_end: date // last business day of closed month
confidence_threshold: float // default 0.85; below this score goes to exceptions
coding_rules: [] // read from Xero bank rules via BankRules endpoint
// Xero pull
bank_transactions: [] // {transaction_id, date, amount, payee, account_code, status: 'UNRECONCILED'|'RECONCILED'}
bank_statement_lines: [] // {statement_line_id, date, amount, description, bank_account_id}
// Hubdoc / Dext pull
supplier_documents: [] // {doc_id, supplier_name, amount, date, document_url, matched_transaction_id|null}
// Output: matched
reconciled_items: [] // {transaction_id, statement_line_id, confidence_score, account_code, doc_id|null}
// Output: exceptions (written to Google Sheets tab 'recon_exceptions')
exception_items: [] // {transaction_id, date, amount, payee, reason: 'no_match'|'low_confidence'|'duplicate_flag', doc_id|null}
// Output: summary row (written to 'recon_summary' tab)
summary: {period_label, total_transactions, matched_count, exception_count, run_timestamp}- Xero API plan requirement: Xero Accounting plan with API access enabled. The Standard or Adviser plan includes this. Confirm the subscription tier before build. OAuth 2.0 PKCE flow is required; store the refresh token in the shared credential store, not in the workflow definition.
- Xero rate limits: 60 API calls per minute per connection, 5,000 calls per day. For a close cycle processing up to 600 transactions, batch all BankTransactions calls with page size of 100 and add a 1-second delay between pages to stay within limits.
- Hubdoc integration: Hubdoc does not publish a public REST API. Use the Hubdoc email-to-inbox feature to push documents to a monitored Gmail inbox, then parse the structured email body to extract supplier_name, amount, and date. Alternatively, use Dext exclusively if the client has an active Dext subscription, as Dext provides a documented REST API.
- Dext API: requires API key from the Dext dashboard (Settings > API). Endpoints used: GET /items (list documents for period), GET /items/{id} (fetch document detail). Confirm API key availability before build.
- Confidence scoring: calculate a weighted match score using: amount exact match (weight 0.5), date within 3 days (weight 0.3), payee string similarity above 0.7 (weight 0.2). Matches scoring 0.85 or above are auto-reconciled. Below 0.85 goes to the exceptions sheet.
- Dedupe: before posting any reconciled match back to Xero, check that the statement_line_id is not already marked RECONCILED in Xero. If it is, skip and log a warning row to the recon_summary tab.
- Confirm with the process owner: how many bank accounts are being reconciled, and whether all accounts have live bank feeds connected in Xero or whether CSV uploads are still needed for any accounts.
- The exceptions Google Sheet must be pre-created with a defined schema (columns: transaction_id, date, amount, payee, reason, doc_id, bookkeeper_action, resolved_at). The agent appends rows; it does not overwrite existing data.
Reads the accruals schedule from a named Google Sheet, calculates the correct journal entries for the period, and posts them to Xero as manual journal lines. Once journals are confirmed as posted, the agent pulls final period figures from the Xero Profit and Loss, Balance Sheet, and Cash Flow Statement reports, populates a Google Sheets management accounts template, generates a variance commentary draft based on configurable thresholds, and exports the completed pack as a PDF. The PDF is held pending Finance Manager approval. On approval, the agent sends the PDF to each stakeholder via Gmail and posts a Slack notification confirming distribution. This agent replaces Steps 6, 7, 8, and 10. Step 9 (Finance Manager approval) is a human gate built into the flow. Estimated build: 18 hours. Complexity: Moderate.
// Input: accruals schedule (read from Google Sheets tab 'accruals_schedule')
accruals: [] // {line_id, description, account_code, debit_amount, credit_amount, period_label, active: bool}
// Input: Xero report pull
xero_tenant_id: string
period_start: date
period_end: date
variance_threshold_pct: float // default 0.10; variances above 10% trigger commentary
// Output: journal posting
posted_journals: [] // {journal_id, xero_journal_id, status: 'posted'|'failed', error_message|null}
// Output: report data (written to Google Sheets tab 'mgmt_accounts_[period_label]')
pl_data: {} // {revenue, cogs, gross_profit, operating_expenses, ebitda, net_profit}
bs_data: {} // {total_assets, total_liabilities, net_assets, equity}
cf_data: {} // {operating_cf, investing_cf, financing_cf, net_change}
variance_commentary: [] // [{line_item, budget_value, actual_value, variance_pct, commentary_text}]
// Output: PDF
report_pdf_url: string // Google Drive link to exported PDF, access restricted to finance team
// On approval (Finance Manager sets approval_status = 'approved' in approval_tracker tab)
distribution_log: [] // {recipient_email, sent_at, gmail_message_id}
slack_notification: {channel: '#finance', message: 'Management accounts for [period_label] have been distributed.', timestamp}- Accruals schedule prerequisite: the Google Sheet tab 'accruals_schedule' must have a consistent column structure before build begins. If the current schedule is ad hoc or varies month to month, the process owner must standardise it first. The agent cannot tolerate variable column positions.
- Xero ManualJournals API: POST to /ManualJournals with an array of JournalLines. Each line requires account_code (string, must match an existing Xero account), line_amount (positive for debit, negative for credit), and description. Validate all account codes against the Xero chart of accounts before the first run.
- Variance commentary: pull prior period figures from the Xero ProfitAndLoss report using the compareWith parameter (e.g. compareWith=1 for one prior period). Calculate variance as (actual - prior) / prior. If variance_pct exceeds variance_threshold_pct, generate a commentary string from a template: '{line_item} is {pct}% {above|below} the prior period at ${actual_value}.'
- PDF export: use the Google Sheets API export endpoint (export?format=pdf) with print settings locked to the named sheet range. Store the exported file in a Google Drive folder named 'Management Accounts / [period_label]' with view access limited to the finance team Google Group.
- Approval gate: poll the 'approval_tracker' Google Sheet tab every 30 minutes for a row where period_label matches and approval_status is set to 'approved' or 'rejected'. On 'rejected', post a Slack alert to the Finance Manager and bookkeeper naming the period. Do not distribute until approval_status is 'approved'.
- Confirm with the process owner: who is on the stakeholder distribution list, and whether any recipients are external (outside the Google Workspace domain), as PDF sharing permissions differ for external addresses.
- Aged payables and receivables (Step 7) are pulled via the Xero Reports API (AgedPayablesByContact, AgedReceivablesByContact) and appended as a tab in the management accounts Google Sheet. No separate output file is needed.
03End-to-end data flow
// ============================================================
// END OF MONTH CLOSE: FULL DATA FLOW
// Trigger to final distribution
// ============================================================
// TRIGGER
// Orchestration layer detects last business day of month
schedule_event: {
trigger_type: 'calendar_schedule',
period_label: 'May 2025',
period_start: '2025-05-01',
period_end: '2025-05-31',
fired_at: '2025-05-30T08:00:00Z'
}
// ============================================================
// AGENT 1: Chase and Notification Agent
// ============================================================
// Read recipient list from Google Sheets tab 'chase_recipients'
input.recipient_list: [
{name, email, slack_user_id, department}
]
input.chase_template_id: 'FullSpec/chase/receipt-request'
input.reminder_delay_hours: 24
// Gmail API: Messages.send per recipient
gmail.send -> {to: recipient.email, subject: 'Action required: receipts for {period_label}', body: rendered_template}
// Slack Bot token: chat.postMessage per recipient
slack.post -> {channel: recipient.slack_user_id, text: 'Please submit receipts for {period_label} by EOD today.'}
// Write delivery log
output.delivery_log -> Google Sheets tab 'delivery_log': {recipient_email, sent_at, channel, status}
// 24h polling: check Gmail thread for replies
gmail.read -> threads where in_reply_to = sent_message_id
output.reply_received -> {recipient_email, replied_at, thread_id}
output.reminder_queue -> recipients where reply_received = false AND elapsed >= 24h
// -- AGENT 1 OUTPUT HANDOFF --
// delivery_log and reply_received passed to orchestration layer
// Orchestration waits 48 hours then triggers Agent 2
// ============================================================
// AGENT 2: Reconciliation Agent
// ============================================================
// Xero API pull: BankTransactions
xero.GET /BankTransactions?where=Date>=period_start&Date<=period_end
-> bank_transactions: [{transaction_id, date, amount, payee, account_code, status}]
// Xero API pull: BankStatements (bank feed lines)
xero.GET /BankStatements?bankAccountID={id}&fromDate=period_start&toDate=period_end
-> bank_statement_lines: [{statement_line_id, date, amount, description, bank_account_id}]
// Xero API pull: BankRules (existing coding rules)
xero.GET /BankRules
-> coding_rules: [{rule_id, conditions, account_code, tracking}]
// Dext API pull
dext.GET /items?from=period_start&to=period_end
-> supplier_documents: [{doc_id, supplier_name, amount, date, document_url}]
// Hubdoc: parse structured emails from Hubdoc inbox
gmail.read (label:'hubdoc-inbox') -> parse {supplier_name, amount, date, document_url}
-> supplier_documents (appended): [{doc_id, supplier_name, amount, date, document_url}]
// Match engine: score each (transaction, statement_line) pair
for each bank_transaction:
score = (amount_exact_match * 0.5)
+ (date_within_3_days * 0.3)
+ (payee_string_similarity_gte_0.7 * 0.2)
if score >= 0.85 -> reconciled_items[]
if score < 0.85 -> exception_items[]
// Link supplier documents to matched transactions
for each reconciled_item:
match supplier_documents where amount == transaction.amount
AND date within 2 days AND supplier_name similarity >= 0.7
-> reconciled_item.doc_id = supplier_document.doc_id
// Write outputs
output.reconciled_items -> Xero: PUT /BankTransactions (status: RECONCILED, account_code)
output.exception_items -> Google Sheets tab 'recon_exceptions':
{transaction_id, date, amount, payee, reason, doc_id, bookkeeper_action='', resolved_at=''}
output.summary -> Google Sheets tab 'recon_summary':
{period_label, total_transactions, matched_count, exception_count, run_timestamp}
// -- AGENT 2 OUTPUT HANDOFF --
// Bookkeeper reviews 'recon_exceptions' tab and sets bookkeeper_action + resolved_at
// Orchestration polls 'recon_exceptions' every 30 min
// When all rows have resolved_at populated -> trigger Agent 3
// ============================================================
// AGENT 3: Accruals and Reporting Agent
// ============================================================
// Read accruals schedule
Google Sheets tab 'accruals_schedule' ->
accruals: [{line_id, description, account_code, debit_amount, credit_amount, period_label, active}]
filter: active == true AND period_label == current_period
// Post journals to Xero
xero.POST /ManualJournals:
{Date: period_end, LineAmountTypes: 'NoTax',
JournalLines: [{AccountCode, Description, LineAmount, TrackingCategories}]}
-> posted_journals: [{line_id, xero_journal_id, status}]
// Pull period reports from Xero
xero.GET /Reports/ProfitAndLoss?fromDate=period_start&toDate=period_end&compareWith=1
-> pl_data: {revenue, cogs, gross_profit, operating_expenses, ebitda, net_profit, prior_period_values}
xero.GET /Reports/BalanceSheet?date=period_end
-> bs_data: {total_assets, total_liabilities, net_assets, equity}
xero.GET /Reports/CashSummary?fromDate=period_start&toDate=period_end
-> cf_data: {operating_cf, investing_cf, financing_cf, net_change}
xero.GET /Reports/AgedPayablesByContact?date=period_end
-> aged_payables: [{contact_name, outstanding_amount, days_overdue}]
xero.GET /Reports/AgedReceivablesByContact?date=period_end
-> aged_receivables: [{contact_name, outstanding_amount, days_overdue}]
// Variance commentary generation
for each pl_line in pl_data:
variance_pct = (actual - prior) / prior
if abs(variance_pct) >= variance_threshold_pct (0.10):
commentary_text = '{line_item} is {pct}% {above|below} the prior period at ${actual_value}.'
-> variance_commentary[]
// Populate management accounts template
Google Sheets tab 'mgmt_accounts_{period_label}':
write pl_data, bs_data, cf_data, aged_payables, aged_receivables, variance_commentary
// Export PDF
Google Sheets export API -> PDF
-> Google Drive folder 'Management Accounts/{period_label}/pack.pdf'
-> report_pdf_url: string (restricted to finance Google Group)
// Approval gate
Google Sheets tab 'approval_tracker':
write {period_label, report_pdf_url, approval_status='pending', approver_email='ml@yourcompany.com'}
// Orchestration polls every 30 min
// Finance Manager sets approval_status = 'approved' | 'rejected'
// On approval_status == 'approved':
gmail.send -> each stakeholder_email:
{subject: 'Management Accounts: {period_label}', attachment: report_pdf_url}
-> distribution_log: [{recipient_email, sent_at, gmail_message_id}]
slack.post -> channel '#finance':
{text: 'Management accounts for {period_label} have been distributed. {report_pdf_url}'}
// On approval_status == 'rejected':
slack.post -> channel '#finance':
{text: 'Management accounts for {period_label} returned for correction. Finance Manager notes: {rejection_note}'}
// ============================================================
// END OF FLOW
// ============================================================04Recommended build stack
More documents for this process
Every document generated for End of Month Close.