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
Purchase Approval Workflow
[YourCompany.com] · Finance Department · Prepared by FullSpec · [Today's Date]
This document provides the full technical specification for the Purchase Approval Workflow automation. It is written for the FullSpec build team and covers the current-state process map, agent-by-agent specifications with IO schemas, the end-to-end data flow, and the recommended build stack. All build decisions, credential management, and testing are handled by FullSpec. The process owner at [YourCompany.com] is responsible for supplying the confirmed spend-threshold rules table and granting tool access before build begins.
01Process snapshot
02Agent specifications
Receives the Jotform webhook payload the moment a purchase request is submitted, validates that all mandatory fields (requester name, department, supplier name, amount, and business justification) are present, assigns a sequential request ID in the format REQ-YYYYMM-NNN, writes a timestamped row to the master purchase register in Google Sheets, and sends the requester an automated Gmail prompt if any required field is absent. Complexity is Moderate because it requires webhook receipt, conditional field validation, ID generation logic, a Sheets write, and a conditional Gmail branch.
// Input
jotform_payload: {
submission_id: string,
requester_name: string,
requester_email: string,
department: string,
supplier_name: string,
amount_usd: number,
justification: string,
quote_url: string | null,
submitted_at: ISO8601
}
// Output (on valid submission)
sheets_row: {
request_id: 'REQ-YYYYMM-NNN',
submission_id: string,
requester_name: string,
requester_email: string,
department: string,
supplier_name: string,
amount_usd: number,
justification: string,
quote_url: string | null,
status: 'Pending',
logged_at: ISO8601
}
// Output (on missing fields)
gmail_resubmit_email: {
to: requester_email,
subject: 'Action required: incomplete purchase request',
body: template_missing_fields_list
}- Jotform plan must support webhook output (Starter tier does not include webhooks; confirm the account is on the Bronze plan or above before build).
- The Google Sheets purchase register must have a named tab 'Purchase Register' with columns matching the output schema exactly; the FullSpec team will create this schema during build.
- Request ID generation must query the current maximum ID from the Sheets register before writing to avoid duplicates under concurrent submissions; use a Sheets LOCK or sequential append approach.
- If the Jotform payload contains a quote_url, the field is written as-is; the agent does not download or validate the linked file.
- The Gmail resubmit prompt lists only the specific missing fields and links back to the Jotform form URL; confirm the form URL is stable (not a draft link) before configuring the template.
- Confirm with the process owner that the Jotform form is the single intake channel before go-live; email and Slack ad-hoc requests must be retired or redirected at launch.
Monitors the purchase register in Google Sheets for rows where status equals 'Pending'. On detecting a new Pending row, it looks up the correct approver from a spend-threshold rules table (a separate named Sheets tab), constructs a Slack Block Kit message containing the request details and inline Approve and Reject buttons, and sends it to the identified approver. A 48-hour timer is set at send time. If no response is recorded before the timer fires, a Gmail escalation email is sent to the approver and their line manager. When the approver responds via Slack interactive callback, the decision, approver Slack user ID, and response timestamp are written back to the purchase register and the row status is updated to 'Approved' or 'Rejected'. Complexity is Complex because it requires polling or change-detection on Sheets, a rules-table lookup with tier matching, Slack Block Kit interactive messaging, a timed escalation branch, and a Sheets write-back from a Slack callback.
// Input
sheets_row: {
request_id: string,
requester_name: string,
department: string,
supplier_name: string,
amount_usd: number,
justification: string,
status: 'Pending',
logged_at: ISO8601
}
rules_table_lookup: {
department: string,
amount_usd: number
-> approver_slack_user_id: string,
approver_email: string,
line_manager_email: string,
tier_label: 'Tier1' | 'Tier2' | 'Tier3'
}
// Output (on Slack response)
sheets_update: {
request_id: string,
status: 'Approved' | 'Rejected',
approver_slack_user_id: string,
decision_timestamp: ISO8601,
rejection_reason: string | null
}
// Output (on 48-hour timeout)
gmail_escalation_email: {
to: approver_email,
cc: line_manager_email,
subject: 'Pending approval: [request_id] awaiting your decision',
body: template_escalation_with_request_detail
}- The spend-threshold rules table must be fully populated and signed off by the process owner before this agent is built; a rules table with gaps or informal tiers cannot drive automated routing.
- Slack Block Kit interactive messages require the automation platform to expose a public HTTPS callback URL; confirm the platform's webhook/callback infrastructure supports this before configuring interactive components.
- The Slack app must be installed to the workspace with scopes: chat:write, users:read, and incoming-webhook at minimum; the Slack app creation and OAuth installation must be completed in the FullSpec sandbox workspace first.
- If an approver's Slack user ID cannot be resolved from the rules table (for example, a new joiner not yet in Slack), the agent must fall back to sending the approval request via Gmail to the approver_email field and log a warning row.
- The 48-hour escalation timer is measured from the Slack message send timestamp, not from the original form submission time; confirm this logic is acceptable with the process owner.
- Rejection responses must capture a free-text reason field in the Slack modal; the Block Kit message must include an input block for this when the Reject button is clicked.
- Dedupe: if the same request_id has already been routed (status no longer 'Pending'), the agent must skip that row to prevent duplicate Slack messages on repeated polling cycles.
Monitors the purchase register for rows where status has just changed to 'Approved' or 'Rejected'. On an Approved status, it calls the Xero Purchases API to create a draft purchase order using the supplier name, approved amount, department, and line item description from the original submission, then sends a Gmail confirmation email to the requester including the Xero PO number. On a Rejected status, it sends a Gmail rejection email to the requester including the recorded rejection reason. The Xero PO is created at Draft status; a finance manager must review and confirm it in Xero before dispatch to the supplier. Complexity is Moderate because it requires a conditional branch on decision status, a Xero API write with field mapping, and two Gmail notification templates.
// Input
sheets_row: {
request_id: string,
requester_name: string,
requester_email: string,
department: string,
supplier_name: string,
amount_usd: number,
justification: string,
status: 'Approved' | 'Rejected',
approver_slack_user_id: string,
decision_timestamp: ISO8601,
rejection_reason: string | null
}
// Output (on Approved)
xero_purchase_order: {
Type: 'PURCHASEORDER',
Status: 'DRAFT',
Contact: { Name: supplier_name },
Date: decision_timestamp (date portion),
LineItems: [
{
Description: justification,
Quantity: 1,
UnitAmount: amount_usd,
AccountCode: department_mapped_account_code
}
],
Reference: request_id
}
gmail_approval_email: {
to: requester_email,
subject: 'Your purchase request [request_id] has been approved',
body: template_approval_with_po_number
}
// Output (on Rejected)
gmail_rejection_email: {
to: requester_email,
subject: 'Your purchase request [request_id] was not approved',
body: template_rejection_with_reason
}
// On approval (retained human step)
xero_manual_review: {
action: 'Finance Manager reviews and confirms draft PO in Xero before supplier dispatch',
po_status_before_review: 'DRAFT',
po_status_after_review: 'SUBMITTED' | 'AUTHORISED'
}- Xero API access requires OAuth 2.0 with scopes: accounting.transactions and accounting.contacts.read; the Xero app must be created in the Xero Developer Portal and connected to the correct Xero organisation before this agent is built.
- The department-to-account-code mapping table must be agreed with the process owner and finance manager before build; the agent cannot create a valid Xero line item without a known account code.
- The Xero Contact (supplier) is matched by name; if the supplier does not exist in Xero, the API will create a new Contact record automatically. Confirm with finance whether new supplier auto-creation is acceptable or whether an existing-contact-only policy applies.
- The Gmail approval email must include the Xero PO number returned in the Xero API response body (PurchaseOrders[0].PurchaseOrderNumber); if the Xero call fails, the agent must log an error row and send an alert rather than silently dropping the notification.
- Rejection emails must include the rejection_reason field verbatim as recorded in the Sheets register; if the field is null, the template must fall back to a default explanation ('No reason was recorded by the approver').
- This agent must not re-process rows it has already handled; implement a 'PO Raised' or 'Notified' status column flag to prevent duplicate PO creation on repeated polling.
03End-to-end data flow
// ─── TRIGGER ───────────────────────────────────────────────────────────────
// Staff member submits Jotform purchase request form
JOTFORM_WEBHOOK_POST -> automation_platform
payload fields: submission_id, requester_name, requester_email,
department, supplier_name, amount_usd,
justification, quote_url, submitted_at
// ─── AGENT 1: Request Intake Agent ─────────────────────────────────────────
// Field validation check
IF any(requester_name, requester_email, department,
supplier_name, amount_usd, justification) IS NULL:
-> GMAIL.send(to: requester_email,
subject: 'Action required: incomplete purchase request',
body: list_of_missing_fields + jotform_form_url)
-> END (await resubmission)
// ID generation and register write
request_id = 'REQ-' + YYYYMM + '-' + SHEETS.max_id_in_register + 1
GOOGLE_SHEETS['Purchase Register'].appendRow({
request_id, submission_id, requester_name, requester_email,
department, supplier_name, amount_usd, justification,
quote_url, status: 'Pending', logged_at: NOW()
})
// -> Handoff to Agent 2 triggered by new Pending row
// ─── AGENT 2: Approval Routing Agent ───────────────────────────────────────
// Rules table lookup
approver = GOOGLE_SHEETS['Rules Table'].lookup(
department == row.department AND
amount_threshold_min <= row.amount_usd <= amount_threshold_max
) -> { approver_slack_user_id, approver_email,
line_manager_email, tier_label }
// Slack interactive message dispatch
SLACK.postMessage(
channel: approver_slack_user_id,
blocks: Block_Kit_card {
request_id, requester_name, department,
supplier_name, amount_usd, justification, quote_url,
actions: [ button('Approve', value:'approved'),
button('Reject', value:'rejected', opens_modal: true) ]
}
)
timer_start = NOW() // 48-hour escalation window begins
// Escalation branch (48 hours elapsed, no response recorded)
IF NOW() - timer_start > 48h AND sheets_row.status == 'Pending':
-> GMAIL.send(
to: approver_email,
cc: line_manager_email,
subject: 'Pending approval: [request_id] awaiting your decision',
body: template_escalation_with_request_detail
)
// Slack callback (approver responds)
SLACK_INTERACTIVE_CALLBACK -> automation_platform
payload fields: action_value ('approved'|'rejected'),
approver_slack_user_id, response_timestamp,
rejection_reason (string | null)
GOOGLE_SHEETS['Purchase Register'].updateRow(request_id, {
status: action_value == 'approved' ? 'Approved' : 'Rejected',
approver_slack_user_id,
decision_timestamp: response_timestamp,
rejection_reason: rejection_reason | null
})
// -> Handoff to Agent 3 triggered by status change to Approved or Rejected
// ─── AGENT 3: PO Creation and Notification Agent ───────────────────────────
// Branch: Approved path
IF status == 'Approved':
xero_response = XERO.PurchaseOrders.create({
Type: 'PURCHASEORDER',
Status: 'DRAFT',
Contact: { Name: supplier_name },
Date: decision_timestamp.date,
LineItems: [{
Description: justification,
Quantity: 1,
UnitAmount: amount_usd,
AccountCode: rules_table.department_account_code
}],
Reference: request_id
})
po_number = xero_response.PurchaseOrders[0].PurchaseOrderNumber
GMAIL.send(
to: requester_email,
subject: 'Your purchase request [request_id] has been approved',
body: template_approval { po_number, supplier_name, amount_usd }
)
// Branch: Rejected path
IF status == 'Rejected':
GMAIL.send(
to: requester_email,
subject: 'Your purchase request [request_id] was not approved',
body: template_rejection { rejection_reason | 'No reason recorded' }
)
// ─── RETAINED HUMAN STEP ───────────────────────────────────────────────────
// Finance Manager reviews DRAFT PO in Xero and confirms before supplier dispatch
// Status transition: DRAFT -> SUBMITTED | AUTHORISED (manual, in Xero UI)
// ─── END OF AUTOMATED FLOW ─────────────────────────────────────────────────04Recommended build stack
More documents for this process
Every document generated for Purchase Approval Workflow.