Back to Vendor & Subscription Renewal Tracking

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

Vendor and Subscription Renewal Tracking

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

This document gives the FullSpec build team everything needed to construct, wire, and validate the Vendor and Subscription Renewal Tracking automation. It covers the current-state step map with bottlenecks flagged, full agent specifications with IO contracts, the end-to-end data flow across all agents, and the recommended build stack with total estimated effort. The process owner's responsibilities are limited to confirming tool access, reviewing UAT outputs, and logging renewal decisions in the live register. Everything else is built and tested by FullSpec.

01Process snapshot

Step
Name
Description
1
Review Subscription Register
IT Manager opens the master Google Sheet manually, typically on a Monday, to check for upcoming renewals. Relies entirely on the individual remembering to act. Time cost: 20 min/cycle.
2
Identify Renewals in the Next 60 Days
Reviewer scans renewal date columns and manually flags rows within 60 days. No formula enforces this consistently, so rows are routinely overlooked. Time cost: 15 min/cycle.
3
Check Vendor Contract Details
For each flagged renewal, the reviewer searches Gmail and shared folders for the original contract or order confirmation to verify pricing, notice period, and auto-renewal terms. Time cost: 25 min/cycle.
4
Assess Whether to Renew or Cancel
IT Manager evaluates whether the tool is still needed, may wait on input from other team leads, and records a decision. This step frequently stalls on multi-department input. Time cost: 30 min/cycle.
5
Notify Finance of Upcoming Charge
IT Manager emails the finance team ad hoc to flag the expected charge and approval needed. Often happens after the charge has already landed. Time cost: 10 min/cycle.
6
Action the Renewal or Cancellation
IT Manager logs into vendor portal, cancels or confirms the subscription, and downloads the updated confirmation or invoice. Time cost: 20 min/cycle.
7
Update the Subscription Register
Reviewer updates the Google Sheet with the new renewal date, decision, and confirmed cost. Frequently skipped under time pressure. Time cost: 10 min/cycle.
8
Log Invoice or Confirm Payment in Xero
Finance Officer locates the invoice, matches it to the expected charge, and codes it in Xero. Discrepancies require manual vendor follow-up. Time cost: 15 min/cycle.
9
File Updated Contract or Confirmation
Updated contract or renewal confirmation is saved to the Notion vendor page. Done inconsistently, making future reviews harder. Time cost: 8 min/cycle.
Time cost summary: Total manual time per cycle is 153 minutes (2 hours 33 min). At a daily check cadence across approximately 35 active contracts, this equates to 4.5 hours of manual effort per week and roughly 20 hours per 30-day period. Steps 2, 3, 7, and 9 are replaced by the Renewal Triage Agent. Steps 4 and 5 are replaced by the Stakeholder Notification Agent. Step 8 is replaced by the Finance Record Agent. Steps 1 and 6 remain manual: the register review trigger is replaced by a scheduled automation, and the vendor portal action stays with the contract owner by design.
Developer Handover PackPage 1 of 4
FS-DOC-04Technical

02Agent specifications

Renewal Triage Agent

Runs daily on a scheduled trigger set at 07:00 AM in the automation platform. Reads every active row from the Google Sheets subscription register and evaluates whether each contract's renewal date falls within the 60-day, 30-day, or 7-day alert window. Checks whether a decision has already been recorded in the Decision Status column to avoid re-alerting on resolved contracts. Categorises each in-window contract by urgency level (High for 7-day, Medium for 30-day, Low for 60-day) and appends a ShortNotice flag where the vendor notice period is shorter than the remaining days before renewal. Outputs a structured list for the Stakeholder Notification Agent. This is a Moderate complexity agent due to the conditional window logic, the dedupe check against existing decisions, and the Notion read used to cross-reference vendor page data.

Trigger
Scheduled daily at 07:00 AM. Fires unconditionally; the agent decides internally whether any contracts require action.
Tools
Google Sheets (read active register rows), Notion (read vendor page metadata for notice period confirmation)
Replaces steps
Steps 2, 3, 7, 9
Estimated build
10 hours, Moderate complexity
// Input
Google Sheets row fields: vendor_name (string), renewal_date (ISO 8601 date), annual_cost (float),
  notice_period_days (integer), auto_renew (boolean), decision_status (string: blank | 'Renew' | 'Cancel'),
  contract_owner_slack_id (string), contract_owner_email (string), finance_contact_email (string),
  notion_page_id (string)

// Output
Array of triage objects: [
  {
    vendor_name: string,
    renewal_date: ISO 8601 date,
    annual_cost: float,
    days_until_renewal: integer,
    urgency_level: 'High' | 'Medium' | 'Low',
    short_notice_flag: boolean,
    decision_status: string,
    contract_owner_slack_id: string,
    contract_owner_email: string,
    finance_contact_email: string,
    notion_page_id: string,
    sheet_row_index: integer
  }
]
// Contracts where decision_status is already 'Renew' or 'Cancel' are excluded from output.
  • Google Sheets connection requires at minimum the spreadsheet-level read scope (spreadsheets.readonly is not sufficient if the Finance Record Agent writes to the same sheet; use spreadsheets scope for the shared credential). The register tab must be named 'Active Contracts' exactly. Any deviation must be confirmed with the process owner before build starts.
  • The renewal date column must contain ISO 8601 date values (YYYY-MM-DD). Mixed formats (e.g. '01 May 2025') will cause the window comparison to fail. The data clean in Week 1 must enforce this format before the agent is connected.
  • Decision Status column must use controlled vocabulary: blank, 'Renew', or 'Cancel'. Any other value (e.g. 'TBC', 'Pending') must be treated as blank and the contract re-alerted. Document this rule in the SOP.
  • Notion API connection uses the Notion Integration Token scoped to the vendor database only. The vendor page is identified by the notion_page_id stored in the Google Sheet, not by vendor name lookup. Confirm all rows have a valid notion_page_id before go-live.
  • The ShortNotice flag fires when days_until_renewal is less than or equal to notice_period_days. If notice_period_days is blank in the sheet, the flag defaults to false and a warning is logged to the error table.
  • Dedupe logic: if decision_status is non-blank, the row is skipped entirely, regardless of urgency window. This prevents re-alerting on actioned contracts.
Stakeholder Notification Agent

Receives the triage output list from the Renewal Triage Agent and composes two sets of outbound messages. First, it sends an individual Slack direct message to each unique contract_owner_slack_id for every contract assigned to them that is in an alert window. The Slack message is structured to include vendor name, renewal date, annual cost, urgency level, any ShortNotice warning, and a plain-text prompt to log the decision in the register. Second, it sends a single consolidated Gmail summary to the finance contact, grouping all contracts due within the next 30 days by urgency and listing expected charges with a direct link to the live Google Sheet. If no contracts are in the triage output, the agent exits without sending any messages. This is a Simple complexity agent: the logic is linear once the triage list is received, and both Slack and Gmail have stable API surfaces.

Trigger
Renewal Triage Agent outputs a non-empty triage list
Tools
Slack (DM per contract owner), Gmail (consolidated summary to finance contact)
Replaces steps
Steps 4, 5
Estimated build
6 hours, Simple complexity
// Input
Triage list from Renewal Triage Agent (array of triage objects as defined above)
Finance contact email: finance_contact_email (string, pulled from first triage object or config)
Google Sheets public URL: register_url (string, stored in workflow config)

// Output (Slack)
Slack DM per contract owner: {
  recipient: contract_owner_slack_id,
  message_blocks: [
    header: '{vendor_name} renewal due in {days_until_renewal} days',
    fields: ['Renewal date', 'Annual cost', 'Urgency', 'Short notice warning (if flag true)'],
    action_prompt: 'Log your decision (Renew or Cancel) in the register: {register_url}'
  ]
}

// Output (Gmail)
Single email to finance_contact_email: {
  subject: 'Upcoming subscription renewals: {N} contracts due in next 30 days',
  body: grouped table of vendor_name, renewal_date, annual_cost, urgency_level,
  footer: register_url
}

// Note: contracts with urgency_level 'Low' (60-day window) are included in Gmail summary
// but excluded from Slack DM unless short_notice_flag is true.
  • Slack connection requires a bot token (xoxb-) with chat:write and users:read scopes. The bot must be added to the workspace and each contract owner must have a valid Slack user ID in the sheet. If a Slack ID is missing or invalid, fall back to a Gmail notification to contract_owner_email and log the fallback event to the error table.
  • Gmail connection uses OAuth 2.0 with the gmail.send scope only. Do not request gmail.readonly or broader scopes. The sending address must be confirmed with the process owner before build, as it determines the From field the finance team sees.
  • Slack message formatting must use Block Kit (section and fields blocks). Plain text messages are acceptable in fallback only. Do not use legacy attachments format.
  • The Gmail finance summary is one email per daily run, not one per contract. Group and sort by urgency_level descending (High first) within the 30-day window. Contracts in the 60-day window that are not within 30 days are excluded from the Gmail summary.
  • If the triage output list is empty (no contracts in any alert window), this agent exits immediately with a logged 'no-op' status. No messages are sent.
  • Confirm with the process owner whether the Slack DM should include a direct link to the specific Google Sheet row or only the top-level register URL. Row-level deep links require the gid parameter to be computed per row and are optional for MVP.
Finance Record Agent

Watches the Google Sheets register for rows where the Decision Status column transitions to 'Renew' (written by the contract owner). On detecting a confirmed renewal, it creates a draft bill in Xero pre-populated with the vendor name mapped to the matching Xero supplier contact, the annual cost as the bill amount, and the renewal date as the due date. The bill is created in Draft status only. It does not submit, approve, or schedule payment. After creating the Xero bill, the agent writes the Xero bill ID and a 'Bill Created' timestamp back to the Google Sheet row so the register reflects the action taken. This is a Moderate complexity agent due to the Xero supplier lookup, the conditional trigger on cell value change, and the write-back to Google Sheets.

Trigger
Google Sheets Decision Status column updated to 'Renew' on any active contract row. Detected via polling interval (recommended: every 15 minutes) or webhook if Google Sheets push notifications are configured.
Tools
Xero (create draft bill, supplier contact lookup), Google Sheets (read confirmed renewal row, write bill ID and timestamp)
Replaces steps
Step 8
Estimated build
8 hours, Moderate complexity
// Input
From Google Sheets confirmed renewal row: {
  vendor_name: string,
  annual_cost: float,
  renewal_date: ISO 8601 date,
  sheet_row_index: integer,
  decision_status: 'Renew'
}

// Xero supplier lookup
GET /Contacts?where=Name=={vendor_name}&IsSupplier=true
Returns: { ContactID: string, Name: string } or empty array
If empty: log error, skip bill creation, alert finance_contact_email via Gmail fallback

// Output (Xero)
POST /Invoices (Type: ACCPAY) {
  Type: 'ACCPAY',
  Status: 'DRAFT',
  Contact: { ContactID: <looked-up ContactID> },
  DueDate: renewal_date,
  LineItems: [{ Description: vendor_name + ' renewal', Quantity: 1, UnitAmount: annual_cost }]
}
Returns: { InvoiceID: string, InvoiceNumber: string, Status: 'DRAFT' }

// Write-back to Google Sheets
Update row at sheet_row_index: {
  xero_bill_id: InvoiceID,
  bill_created_at: ISO 8601 datetime
}

// On approval (manual, finance officer only)
Finance officer reviews draft bill in Xero and approves or deletes it manually.
No automation action is taken after bill creation.
  • Xero connection uses OAuth 2.0 with scopes: accounting.transactions (create invoices), accounting.contacts.read (lookup supplier contacts). The Xero tenant ID must be stored in the shared credential store. If the account uses multiple Xero organisations, confirm the correct tenant ID before build starts.
  • Vendor name matching to Xero supplier contacts is case-insensitive exact match on the Contact Name field. Partial matches are not used. If no match is found, the agent logs an error and sends a Gmail alert to the finance contact with the vendor name, amount, and instruction to create the supplier contact manually.
  • The Google Sheets polling trigger checks for rows where Decision Status equals 'Renew' and xero_bill_id is blank. This prevents duplicate bill creation if the agent is re-run. The xero_bill_id column must exist in the register schema before build starts.
  • The Xero sandbox (Demo Company) must be used for all testing. Do not create draft bills in the live Xero organisation during UAT. Confirm sandbox access with the Finance Officer before the build week begins.
  • Annual cost in the register must be stored as a numeric value (float), not as a formatted currency string (e.g. '$1,200.00'). The data clean must enforce this. If a string is detected, the agent logs a parse error and skips bill creation for that row.
  • This agent does not handle cancellations. Rows where Decision Status is 'Cancel' are ignored entirely. No Xero action is taken for cancelled contracts.
Developer Handover PackPage 2 of 4
FS-DOC-04Technical

03End-to-end data flow

Full data flow: Daily schedule trigger through triage, notification, and Xero bill creation. Field names match the Google Sheets register schema agreed during the Week 1 data clean.
// ============================================================
// VENDOR AND SUBSCRIPTION RENEWAL TRACKING
// End-to-end data flow: trigger to final output
// ============================================================

// TRIGGER
// Automation platform scheduler fires daily at 07:00 AM
trigger: {
  type: 'schedule',
  time: '07:00',
  timezone: 'business_timezone_from_config'
}

// ============================================================
// STEP 1: Read subscription register from Google Sheets
// ============================================================
google_sheets.getRows({
  spreadsheet_id: config.REGISTER_SPREADSHEET_ID,
  sheet_name: 'Active Contracts',
  fields: [
    'vendor_name', 'renewal_date', 'annual_cost',
    'notice_period_days', 'auto_renew', 'decision_status',
    'contract_owner_slack_id', 'contract_owner_email',
    'finance_contact_email', 'notion_page_id', 'xero_bill_id'
  ]
})
// Returns: rows[] where each row maps to one active contract

// ============================================================
// AGENT HANDOFF 1: Raw register rows -> Renewal Triage Agent
// ============================================================

// RENEWAL TRIAGE AGENT
// For each row, compute days_until_renewal = renewal_date - today()
for each row in rows:
  days_until_renewal = dateDiff(today(), row.renewal_date)  // integer, days

  // Determine urgency window
  if days_until_renewal <= 7:  urgency_level = 'High'
  elif days_until_renewal <= 30: urgency_level = 'Medium'
  elif days_until_renewal <= 60: urgency_level = 'Low'
  else: skip row  // outside alert window

  // Dedupe check: skip if decision already recorded
  if row.decision_status in ['Renew', 'Cancel']: skip row

  // Short notice flag
  short_notice_flag = (days_until_renewal <= row.notice_period_days)

  // Cross-reference Notion for notice period confirmation
  notion.getPage({ page_id: row.notion_page_id })
  // Returns: { notice_period_days (if overrides sheet value), last_updated }

  // Append to triage_list
  triage_list.append({
    vendor_name, renewal_date, annual_cost,
    days_until_renewal, urgency_level, short_notice_flag,
    decision_status, contract_owner_slack_id,
    contract_owner_email, finance_contact_email,
    notion_page_id, sheet_row_index
  })

// ============================================================
// AGENT HANDOFF 2: triage_list -> Stakeholder Notification Agent
// ============================================================

if triage_list.length == 0:
  log('no-op: no contracts in alert window')
  exit

// STAKEHOLDER NOTIFICATION AGENT
// Group triage_list by contract_owner_slack_id
owner_groups = groupBy(triage_list, 'contract_owner_slack_id')

for each owner_slack_id, contracts in owner_groups:
  // Send Slack DM per contract owner
  slack.postMessage({
    channel: owner_slack_id,
    blocks: buildSlackBlocks({
      contracts: contracts,  // vendor_name, renewal_date, annual_cost,
                             // urgency_level, short_notice_flag
      register_url: config.REGISTER_URL
    })
  })
  // On Slack ID missing: fallback to gmail.send -> contract_owner_email
  // Log fallback event to error_log table

// Build Gmail finance summary (contracts due within 30 days only)
finance_contracts = triage_list.filter(c => c.days_until_renewal <= 30)
finance_contracts.sortBy('urgency_level', descending=true)

gmail.send({
  to: triage_list[0].finance_contact_email,
  subject: `Upcoming subscription renewals: ${finance_contracts.length} contracts due in next 30 days`,
  body: buildFinanceSummaryHTML({
    contracts: finance_contracts,  // vendor_name, renewal_date, annual_cost, urgency_level
    register_url: config.REGISTER_URL
  })
})

// ============================================================
// MANUAL STEP: Contract owner logs decision in Google Sheets
// decision_status column updated to 'Renew' or 'Cancel'
// This step is NOT automated. Owner acts on Slack prompt.
// ============================================================

// ============================================================
// AGENT HANDOFF 3: Decision detected -> Finance Record Agent
// Poll trigger: every 15 min, check for rows where
// decision_status == 'Renew' AND xero_bill_id IS BLANK
// ============================================================

// FINANCE RECORD AGENT
new_renewals = google_sheets.getRows({
  filter: 'decision_status == Renew AND xero_bill_id == blank'
})

for each renewal in new_renewals:
  // Lookup Xero supplier contact
  contact = xero.getContacts({
    where: `Name=="${renewal.vendor_name}" AND IsSupplier==true`
  })
  // Returns: { ContactID, Name } or empty

  if contact is empty:
    log.error({ vendor_name, reason: 'Xero supplier not found' })
    gmail.send({ to: finance_contact_email, subject: 'Action required: create Xero supplier for ' + vendor_name })
    continue

  // Create draft bill in Xero
  bill = xero.createInvoice({
    Type: 'ACCPAY',
    Status: 'DRAFT',
    Contact: { ContactID: contact.ContactID },
    DueDate: renewal.renewal_date,
    LineItems: [{
      Description: renewal.vendor_name + ' renewal',
      Quantity: 1,
      UnitAmount: renewal.annual_cost
    }]
  })
  // Returns: { InvoiceID, InvoiceNumber, Status: 'DRAFT' }

  // Write-back to Google Sheets register
  google_sheets.updateRow({
    row_index: renewal.sheet_row_index,
    fields: {
      xero_bill_id: bill.InvoiceID,
      bill_created_at: now()  // ISO 8601 datetime
    }
  })

// ============================================================
// MANUAL STEP: Finance Officer reviews and approves draft bill in Xero
// No further automation action. Flow ends.
// ============================================================
Developer Handover PackPage 3 of 4
FS-DOC-04Technical

04Recommended build stack

Orchestration layer
A workflow automation tool running one workflow per agent (three workflows total: Renewal Triage, Stakeholder Notification, Finance Record). Each workflow uses a shared credential store so Google Sheets, Slack, Gmail, Xero, and Notion tokens are defined once and referenced by all workflows. Credentials are never hardcoded in workflow nodes.
Webhook configuration
The Renewal Triage and Stakeholder Notification agents are triggered by the internal scheduler (no inbound webhook required). The Finance Record Agent uses a polling-based trigger against Google Sheets (15-minute interval) unless the automation platform supports native Google Sheets change webhooks, in which case a push trigger is preferred to reduce latency. No public-facing webhook endpoints are required for this process.
Templating approach
Slack Block Kit JSON templates are built as reusable functions within the Stakeholder Notification Agent workflow, parameterised with vendor_name, renewal_date, annual_cost, urgency_level, short_notice_flag, and register_url. Gmail HTML body uses a plain table template stored as a workflow variable. Both templates are version-controlled alongside the workflow definitions. No external templating service is required.
Error logging
All agent errors (missing Notion page IDs, invalid Xero supplier lookups, missing Slack IDs, unparseable date or cost values) are written to a Supabase table named renewal_automation_errors with fields: error_id (uuid), agent_name (string), vendor_name (string), error_type (string), error_detail (string), occurred_at (ISO 8601 datetime), resolved (boolean). A Supabase database webhook triggers a Slack alert to the FullSpec monitoring channel whenever a new unresolved error row is inserted. The process owner is notified only for errors that require their action (e.g. missing Xero supplier).
Testing approach
All three agents are built and tested against sandbox environments first: Xero Demo Company for Finance Record Agent, a staging copy of the Google Sheet (cloned from the production register with anonymised test vendors), and a dedicated Slack test channel for Notification Agent output. No production Xero organisation, live Slack channels, or the real finance contact email are used during build or UAT. Production credentials are only connected after UAT sign-off is confirmed.
Renewal Triage Agent build time
10 hours (Moderate)
Stakeholder Notification Agent build time
6 hours (Simple)
Finance Record Agent build time
8 hours (Moderate)
End-to-end integration testing
4 hours (cross-agent flow, error path validation, UAT support)
Estimated total build time
28 hours across all agents and end-to-end testing, consistent with the confirmed build effort in the process snapshot.
Before any workflow is connected to production credentials, the following must be confirmed: (1) The Google Sheets register uses the agreed column schema and ISO 8601 date format throughout. (2) All active vendors exist as supplier contacts in the live Xero organisation. (3) All contract owners have valid Slack user IDs recorded in the register. (4) The Notion integration token is scoped to the vendor database only and all register rows contain a valid notion_page_id. Raise any gaps with the process owner during the Week 1 register audit before build starts. Contact support@gofullspec.com for any access or credential questions.
Developer Handover PackPage 4 of 4

More documents for this process

Every document generated for Vendor & Subscription Renewal Tracking.

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