Back to Expense Management

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

Expense Management Automation

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

This document is the authoritative technical reference for the FullSpec team building the Expense Management automation. It covers the current-state process map with bottleneck identification, full specifications for each of the three agents, end-to-end data flow, and the recommended build stack. Everything in this pack has been derived from the confirmed process template and agreed tooling list. The FullSpec team owns all build, integration, and testing activity described here. Your team's responsibilities are limited to providing API credentials, confirming the GL mapping table and expense policy rules, and signing off on test outputs before go-live.

01Process snapshot

Step
Name
Description
1
Employee Submits Expense Claim
Employee emails or spreadsheet-submits a description, amount, and receipt to finance. Formatting is inconsistent across staff. Time cost: 8 min (employee-side, not automated).
2
Finance Team Checks Receipt Attached
Finance admin opens each submission and confirms a legible receipt is attached and the amount matches the claim. Missing receipts trigger a follow-up. Time cost: 6 min.
3
Chase Missing Receipts
Finance admin sends follow-up emails and waits for a response, often repeating two or three times. This step is a primary bottleneck. Time cost: 10 min per occurrence.
4
Check Against Expense Policy
Finance admin manually checks category, amount, and merchant against the company expense policy. Time cost: 7 min.
5
Assign GL Code and Category
Admin manually assigns the correct GL code and expense category by referencing Xero's chart of accounts. Time cost: 5 min.
6
Route Claim to Approving Manager
Finance admin forwards the coded claim via Gmail with no tracking mechanism. Claims can sit unactioned for days. This step is a primary bottleneck. Time cost: 4 min.
7
Manager Reviews and Approves or Rejects
Manager reads the email and replies to approve or reject. Rejected claims are relayed back by finance admin. Time cost: 5 min.
8
Log Approved Claim in Spreadsheet
Finance admin records the approved claim in a master Google Drive tracking spreadsheet. Time cost: 4 min.
9
Create Bill or Spend Record in Xero
Admin manually creates a spend money transaction in Xero, enters all fields, and attaches the receipt file. Time cost: 6 min.
10
Process Reimbursement via Gusto
Finance admin adds the approved amount to the next payroll run in Gusto or initiates a manual bank transfer. Time cost: 5 min.
11
Notify Employee of Reimbursement
Admin sends a brief Gmail confirmation that the claim is approved and states the payment date. Time cost: 3 min.
Time cost summary: Total manual time per claim cycle is 63 minutes (steps 2 through 11, excluding employee submission). At approximately 120 claims per month across roughly 30 working claims per week, this equates to 6 manual hours per week and approximately 26 hours per 30-day period. The automation replaces steps 2, 3, 4, 5, 6, 8, 9, 10, and 11 entirely. Step 7 (manager approval) is preserved as a human decision but is re-routed through Slack with enforced structure and reminders. Step 1 (employee submission) is migrated to Expensify as the new intake point.
Developer Handover PackPage 1 of 4
FS-DOC-04Technical

02Agent specifications

Receipt Extraction and Policy Agent

This agent handles the first stage of the automated pipeline. On each new Expensify claim submission it reads the attached receipt image or PDF using OCR, extracts the structured fields (amount, date, merchant name, currency, and expense category), and immediately runs each field through the configured expense policy rule set. Claims that pass all rules are marked as verified and handed off to the next agent. Claims that fail are flagged with a specific machine-readable reason code and a human-readable description, and an automated receipt chase email is dispatched via Gmail. This agent eliminates the manual receipt-checking, chasing, and policy-review steps that currently consume the largest share of finance admin time. Estimated build time: 10 hours. Complexity: Moderate.

Trigger
New expense claim submitted in Expensify (webhook or polling interval, to be confirmed based on Expensify plan tier).
Tools
Expensify, Gmail, Google Drive
Replaces steps
2 (receipt check), 3 (receipt chase), 4 (policy check)
Estimated build
10 hours / Moderate
// Input
expensify.claim {
  claim_id: string,
  employee_email: string,
  submission_timestamp: ISO8601,
  amount: float,
  currency: string,
  merchant_name: string,
  merchant_category: string,
  receipt_url: string | null,
  description: string
}

// Output (passing claim)
verified_claim {
  claim_id: string,
  employee_email: string,
  amount_usd: float,
  merchant_name: string,
  merchant_category: string,
  receipt_gdrive_url: string,
  policy_verdict: 'pass',
  extracted_date: ISO8601
}

// Output (failing claim)
flagged_claim {
  claim_id: string,
  employee_email: string,
  policy_verdict: 'fail',
  flag_reason_code: string,   // e.g. 'MISSING_RECEIPT' | 'OVER_LIMIT' | 'CATEGORY_DISALLOWED'
  flag_reason_human: string,
  chase_email_sent: boolean,
  chase_timestamp: ISO8601
}
  • Expensify API access requires the Expensify Control plan or higher for full API reporting and webhook support. Confirm the client's current plan before build starts. If on a lower tier, a polling-based approach using the Export Reports endpoint must be used instead.
  • OCR extraction should be performed via the automation platform's built-in document parsing node or a connected document intelligence service. Do not rely on Expensify's native OCR output as the sole source; validate extracted fields against the raw receipt where possible.
  • The expense policy rule set must be received as a structured document (JSON or spreadsheet) before this agent is built. Ambiguous rules will produce inconsistent verdicts and must be resolved with the finance manager before configuration.
  • Receipt files must be downloaded from Expensify and stored in a designated Google Drive folder (path: /ExpenseReceipts/{YYYY-MM}/{claim_id}) before policy evaluation, to ensure a persistent copy exists independent of Expensify retention settings.
  • Chase emails must use a templated Gmail draft with a fixed reply-to address monitored by the automation platform. Implement a maximum of three chase attempts with a 48-hour interval, then escalate to finance admin via Slack after the third failed attempt.
  • Deduplicate incoming claims by claim_id before processing. If a duplicate claim_id is detected, log the event and suppress further processing without sending a second chase email.
  • Fallback: if OCR extraction confidence is below the threshold configured in the policy (recommended: 85%), treat the claim as a manual review item and route to finance admin rather than auto-rejecting.
GL Coding and Xero Sync Agent

This agent receives a verified claim record from the Receipt Extraction and Policy Agent and performs two tasks: it maps the merchant category and description to the correct Xero general ledger code using a pre-built merchant-to-GL mapping table, then creates and populates a draft spend money record in Xero with all fields pre-filled and the receipt file attached. The agent replaces the manual GL coding step and the manual Xero data entry step, which together account for 11 minutes of admin time per claim. The mapping table is maintained as a configuration file outside the agent logic so it can be updated by the finance team without modifying the build. Estimated build time: 8 hours. Complexity: Moderate.

Trigger
Receipt Extraction and Policy Agent emits a passing verified_claim record.
Tools
Xero, Expensify
Replaces steps
5 (GL code assignment), 8 (spreadsheet log), 9 (Xero spend record creation)
Estimated build
8 hours / Moderate
// Input
verified_claim {
  claim_id: string,
  employee_email: string,
  amount_usd: float,
  merchant_name: string,
  merchant_category: string,
  receipt_gdrive_url: string,
  policy_verdict: 'pass',
  extracted_date: ISO8601
}

// GL mapping lookup
gl_mapping_table[merchant_category] -> {
  xero_account_code: string,  // e.g. '420' for Travel, '425' for Meals
  xero_account_name: string,
  tax_type: string            // e.g. 'NONE' | 'TAX'
}

// Output
xero_draft_record {
  xero_transaction_id: string,
  claim_id: string,
  status: 'DRAFT',
  contact_name: string,       // employee full name from Expensify profile
  date: ISO8601,
  amount: float,
  account_code: string,
  account_name: string,
  description: string,
  receipt_attachment_url: string,
  reference: string           // claim_id as Xero reference field
}
  • Xero integration requires OAuth 2.0 with the accounting.transactions scope (read and write) and the files scope for receipt attachment. Confirm that the Xero organisation ID is available and that the connecting user has Standard or Adviser role.
  • The merchant-to-GL mapping table must be provided by the finance manager as a confirmed, reviewed spreadsheet before this agent is built. Store it as an external configuration object (JSON or a dedicated config table) so the finance team can update mappings without a code change.
  • If a merchant category cannot be matched in the mapping table, do not guess. Route the claim to finance admin for manual GL assignment and log the unmapped category for table maintenance.
  • Xero draft records must use status 'DRAFT' (not 'AUTHORISED') so a human can still review before finalisation. The Approval Routing and Notification Agent is responsible for moving the record to 'AUTHORISED' status.
  • Receipt attachment to the Xero transaction must use the Xero Files API. Upload the file from the Google Drive URL stored in the previous step. Do not re-download from Expensify at this stage.
  • The spreadsheet log (step 8 in the manual process) is eliminated by using the Xero draft record as the system of record. Confirm with the finance manager that the Google Drive spreadsheet can be retired post-go-live.
Approval Routing and Notification Agent

This agent takes a confirmed Xero draft record and manages the full approval lifecycle: it identifies the correct approving manager based on a configurable manager assignment matrix (by department or cost centre), sends a structured Slack message with claim details and interactive approve or reject buttons, fires a 24-hour reminder if no action is taken, finalises the Xero record to 'AUTHORISED' status on approval, adds the reimbursement line to the employee's next Gusto payroll run, and sends a confirmation email to the employee via Gmail. On rejection, it posts the rejection reason back to the finance admin via Slack and sends the employee a rejection notification via Gmail. Estimated build time: 10 hours. Complexity: Moderate.

Trigger
GL Coding and Xero Sync Agent emits a confirmed xero_draft_record.
Tools
Slack, Gmail, Xero, Gusto
Replaces steps
6 (approval routing), 7 (manager review, preserved as human action but structured), 10 (Gusto reimbursement), 11 (employee notification)
Estimated build
10 hours / Moderate
// Input
xero_draft_record {
  xero_transaction_id: string,
  claim_id: string,
  contact_name: string,
  employee_email: string,
  date: ISO8601,
  amount: float,
  account_code: string,
  account_name: string,
  description: string,
  receipt_attachment_url: string
}

// Manager assignment lookup
manager_matrix[employee_email | department] -> {
  manager_slack_id: string,
  manager_email: string
}

// On approval
approval_payload {
  claim_id: string,
  xero_transaction_id: string,
  approved_by_slack_id: string,
  approval_timestamp: ISO8601
}
-> xero.updateTransaction(status: 'AUTHORISED')
-> gusto.addReimbursementLine {
     employee_id: string,
     amount: float,
     description: string,   // 'Expense reimbursement: {claim_id}'
     payroll_period: string
   }
-> gmail.send { to: employee_email, template: 'claim_approved' }

// On rejection
rejection_payload {
  claim_id: string,
  rejected_by_slack_id: string,
  rejection_reason: string,
  rejection_timestamp: ISO8601
}
-> slack.notify { channel: finance_admin_slack_id, message: rejection_summary }
-> gmail.send { to: employee_email, template: 'claim_rejected', reason: rejection_reason }
  • Slack integration requires a custom Slack app with bot token scopes: chat:write, im:write, and the ability to post interactive messages using Block Kit with button actions. Confirm the Slack workspace plan supports interactive components (available on Pro and above).
  • The manager assignment matrix must be provided before build and stored as an external configuration object. It must map either employee email or department/cost centre to a Slack user ID and email address. Edge cases (e.g. the manager is the claimant) must be defined in the matrix.
  • The 24-hour reminder must be implemented as a scheduled re-check on the claim's pending status, not as a duplicate Slack message thread. Use the original message's Slack ts value to post the reminder as a thread reply.
  • Gusto reimbursement API integration requires Gusto admin credentials with payroll:write scope. FullSpec recommends a manual review of the first two to three reimbursement batches before finalisation to confirm payroll period alignment.
  • Gmail notifications must use templated drafts stored in the automation platform. The 'claim_approved' template must include the payroll date, amount, and a reference to the claim_id. The 'claim_rejected' template must include the rejection reason verbatim.
  • Xero status transition from 'DRAFT' to 'AUTHORISED' must only occur after the Slack approval payload is received and validated. Never auto-authorise on a timeout.
  • If the Slack approval action is not received within 48 hours after the reminder, escalate to the finance admin via a direct Slack message listing all pending approvals. Do not finalise or reject automatically.
Developer Handover PackPage 2 of 4
FS-DOC-04Technical

03End-to-end data flow

Full end-to-end data flow: Expense Management Automation. Field names are exact. Comments mark agent handoffs.
// ─── TRIGGER ────────────────────────────────────────────────────────────────
// Employee submits expense claim via Expensify
expensify.webhook -> automation_platform.receive {
  claim_id: 'EXP-{YYYYMMDD}-{seq}',
  employee_email: string,
  submission_timestamp: ISO8601,
  amount: float,
  currency: string,
  merchant_name: string,
  merchant_category: string,  // Expensify-assigned category
  receipt_url: string | null,
  description: string
}

// ─── DEDUPLICATE ─────────────────────────────────────────────────────────────
IF claim_id EXISTS IN processed_claims_log -> HALT (suppress duplicate)
ELSE -> CONTINUE, write claim_id to processed_claims_log

// ─── AGENT 1: Receipt Extraction and Policy Agent ────────────────────────────
// Step A: Download and store receipt
GET expensify.receipt_url
  -> store to Google Drive: /ExpenseReceipts/{YYYY-MM}/{claim_id}.{ext}
  -> receipt_gdrive_url: string

// Step B: OCR extraction
ocr_engine.extract(receipt_gdrive_url) -> {
  ocr_amount: float,
  ocr_date: ISO8601,
  ocr_merchant: string,
  ocr_confidence: float       // 0.0 - 1.0
}

// Step C: Confidence gate
IF ocr_confidence < 0.85
  -> route to finance_admin (manual review), HALT agent pipeline

// Step D: Policy check
policy_engine.evaluate {
  amount: ocr_amount,
  merchant_category: merchant_category,
  receipt_present: receipt_url != null,
  policy_rules: config.expense_policy_rules
} -> {
  policy_verdict: 'pass' | 'fail',
  flag_reason_code: string | null,
  flag_reason_human: string | null
}

// Step E: Fail path - send receipt chase via Gmail
IF policy_verdict == 'fail'
  -> gmail.send {
       to: employee_email,
       template: 'receipt_chase',
       reason: flag_reason_human,
       reply_to: 'expenses-intake@[YourCompany.com]',
       attempt_number: int  // max 3, interval 48h
     }
  -> write chase_log { claim_id, attempt_number, chase_timestamp }
  -> HALT (await resubmission)

// Step F: Pass path - emit verified_claim
IF policy_verdict == 'pass'
  -> verified_claim {
       claim_id, employee_email, amount_usd: ocr_amount,
       merchant_name: ocr_merchant, merchant_category,
       receipt_gdrive_url, policy_verdict: 'pass',
       extracted_date: ocr_date
     }

// ─── AGENT 2: GL Coding and Xero Sync Agent ─────────────────────────────────
// Step G: GL mapping lookup
gl_mapping_table.lookup(merchant_category) -> {
  xero_account_code: string,
  xero_account_name: string,
  tax_type: string
}

IF no match found
  -> route to finance_admin (unmapped category), log unmapped_category_event, HALT

// Step H: Create Xero draft spend money record
xero.createSpendMoneyTransaction {
  status: 'DRAFT',
  contact_name: expensify.employee_full_name,
  date: extracted_date,
  amount: amount_usd,
  account_code: xero_account_code,
  description: description,
  reference: claim_id,
  tax_type: tax_type
} -> xero_transaction_id: string

// Step I: Attach receipt to Xero record
xero.files.upload {
  transaction_id: xero_transaction_id,
  file_url: receipt_gdrive_url,
  filename: '{claim_id}_receipt.{ext}'
}

// Step J: Emit xero_draft_record
xero_draft_record {
  xero_transaction_id, claim_id,
  contact_name, employee_email,
  date: extracted_date, amount: amount_usd,
  account_code: xero_account_code,
  account_name: xero_account_name,
  description, receipt_attachment_url: receipt_gdrive_url
}

// ─── AGENT 3: Approval Routing and Notification Agent ───────────────────────
// Step K: Manager lookup
manager_matrix.lookup(employee_email) -> {
  manager_slack_id: string,
  manager_email: string
}

// Step L: Send Slack approval request
slack.postMessage {
  channel: manager_slack_id,
  blocks: [
    { type: 'section', text: 'Expense claim pending approval' },
    { type: 'fields', fields: [amount, merchant_name, account_name, date] },
    { type: 'actions', elements: [
        { action_id: 'approve_claim', text: 'Approve' },
        { action_id: 'reject_claim',  text: 'Reject'  }
      ]
    }
  ],
  metadata: { claim_id, xero_transaction_id }
} -> slack_message_ts: string

// Step M: 24-hour reminder (scheduled)
IF no action after 24h
  -> slack.postMessage (thread_ts: slack_message_ts) { text: 'Reminder: claim awaiting approval' }
IF no action after 48h
  -> slack.postMessage { channel: finance_admin_slack_id, text: 'Escalation: claim {claim_id} unanswered' }

// Step N: Approval path
ON slack.action(action_id: 'approve_claim') -> {
  approved_by_slack_id, approval_timestamp
}
  -> xero.updateTransaction { id: xero_transaction_id, status: 'AUTHORISED' }
  -> gusto.addReimbursementLine {
       employee_id: gusto.lookupByEmail(employee_email),
       amount: amount_usd,
       description: 'Expense reimbursement: {claim_id}',
       payroll_period: gusto.currentOpenPeriod
     }
  -> gmail.send { to: employee_email, template: 'claim_approved',
       vars: { amount: amount_usd, payroll_date: gusto.nextPayrollDate } }
  -> write approval_log { claim_id, xero_transaction_id, approved_by_slack_id, approval_timestamp }

// Step O: Rejection path
ON slack.action(action_id: 'reject_claim') -> {
  rejected_by_slack_id, rejection_reason, rejection_timestamp
}
  -> slack.postMessage { channel: finance_admin_slack_id,
       text: 'Claim {claim_id} rejected: {rejection_reason}' }
  -> gmail.send { to: employee_email, template: 'claim_rejected',
       vars: { reason: rejection_reason } }
  -> write rejection_log { claim_id, rejected_by_slack_id, rejection_reason, rejection_timestamp }

// ─── END OF PIPELINE ─────────────────────────────────────────────────────────
Developer Handover PackPage 3 of 4
FS-DOC-04Technical

04Recommended build stack

Orchestration layer
A workflow automation platform (tool to be confirmed at build kickoff). Implement one workflow per agent: 'Receipt Extraction and Policy', 'GL Coding and Xero Sync', and 'Approval Routing and Notification'. All three workflows share a single credential store within the platform, with named credential entries for Expensify, Xero, Slack, Gmail, Google Drive, and Gusto. No credentials are hardcoded in workflow nodes.
Webhook configuration
Agent 1 is triggered by an Expensify webhook (POST to the automation platform's inbound webhook URL, secured with a shared secret in the request header). If the Expensify plan does not support outbound webhooks, replace with a polling workflow on a 5-minute interval using the Expensify Export Reports API. Agent 2 and Agent 3 are triggered by internal workflow-to-workflow handoffs (not external webhooks). Slack interactive action callbacks (approve/reject) are received via a dedicated Slack Request URL registered in the Slack app manifest.
Templating approach
All outbound emails (receipt chase, claim approved, claim rejected) use template strings stored as workflow environment variables, not inline node text, so they can be updated without modifying workflow logic. Slack Block Kit message structures are stored as JSON configuration objects and referenced by the approval routing workflow. The GL mapping table and expense policy rules are stored as JSON configuration objects in the platform's environment or a dedicated config table, not hardcoded.
Error logging
All error events (OCR confidence failures, unmapped GL categories, Xero API errors, Gusto API errors, Slack action timeouts) are written to a dedicated error log table (recommended: Supabase table 'expense_automation_errors' with columns: error_id, claim_id, agent_name, error_code, error_detail, timestamp). A Slack alert is sent to the finance admin Slack channel for any error that halts a claim without routing it to a human. Non-fatal warnings (e.g. reminder sent) are logged to a separate audit table without generating a Slack alert.
Testing approach
All three agents must be built and tested in a sandbox environment first: Expensify sandbox account, Xero demo company, Gusto sandbox, and a dedicated Slack test workspace. No production data is used during build. End-to-end testing covers compliant claims, missing-receipt claims, policy-violating claims, manager approval, manager rejection, and the 24-hour reminder path. Production credentials are introduced only after QA sign-off.
Estimated total build time
Agent 1 (Receipt Extraction and Policy Agent): 10 hours. Agent 2 (GL Coding and Xero Sync Agent): 8 hours. Agent 3 (Approval Routing and Notification Agent): 10 hours. End-to-end integration testing and QA: 8 hours (included in the 28-hour total per the confirmed build estimate). Total: 28 hours.
Three items must be in hand before build begins: (1) the expense policy rules as a structured document signed off by the finance manager, (2) the merchant-to-GL mapping table reviewed against the current Xero chart of accounts, and (3) confirmed API credentials and plan tiers for Expensify, Xero, Gusto, and Slack. Build cannot progress past Agent 1 without items 1 and 3, and cannot progress past Agent 2 without item 2. Contact the FullSpec team at support@gofullspec.com if any of these items are not yet available.
Developer Handover PackPage 4 of 4

More documents for this process

Every document generated for Expense Management.

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