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
Asset and Equipment Tracking
[YourCompany.com] · Operations Department · Prepared by FullSpec · [Today's Date]
This document is the primary technical reference for the FullSpec team building the Asset and Equipment Tracking automation. It covers the full current-state process, all three agent specifications, the end-to-end data flow, and the recommended build stack. The build replaces nine of eleven manual steps, leaving only physical QR label attachment and manager audit sign-off as human actions. Every constraint, field name, tool tier, and integration behaviour described here is binding for the build.
01Process snapshot
02Agent specifications
Validates every new or updated asset record in Airtable for field completeness, auto-generates a unique sequential asset ID if one is absent, calls the QR Code Generator API to produce a printable label URL, writes that URL back to the Airtable record, and sends a Slack assignment notification to the named assignee. This agent replaces the four highest-frequency manual steps, reducing new-asset intake from 35 minutes to under 2 minutes. It must handle both net-new rows and material updates (for example, a reassignment or a serial-number correction) without creating duplicate asset IDs.
// Input
airtable.record_id // internal Airtable row ID
airtable.asset_name // string, required
airtable.serial_number // string, required
airtable.purchase_date // ISO 8601 date, required
airtable.purchase_cost // number (USD), required
airtable.assigned_to // Airtable linked record -> staff_member.slack_user_id
airtable.asset_category // select field: IT Hardware | Tools | Furniture | Vehicle | Other
airtable.site_location // string, optional
// Validation rules
// asset_name, serial_number, purchase_date, purchase_cost must be non-null
// If asset_id is null -> generate: ASSET-{YYYY}-{zero-padded sequence, 4 digits}
// If asset_id already exists -> skip ID generation, proceed to QR step
// Duplicate check: query Airtable for existing serial_number before write
// Output
airtable.asset_id // written back to record, e.g. ASSET-2025-0047
airtable.qr_code_url // written back to record, public URL from QR API
airtable.intake_status // set to 'Confirmed'
slack.message_ts // Slack message timestamp, stored for audit trail
// Slack notification payload
slack.channel // resolved from assigned_to.slack_user_id (DM)
slack.text // 'Asset {asset_name} ({asset_id}) assigned to you. QR label: {qr_code_url}. Please check care instructions in the register.'- Airtable plan must be Pro or above for automation triggers via webhook; confirm the base shares API access with the automation platform service account before build.
- QR Code Generator API: use the QR Server free tier (api.qrserver.com/v1/create-qr-code/) or the GoQR.me endpoint. The URL embedded in the QR code must be the permanent Airtable record URL, not a short-lived share link. Store the full image URL, not the base64 blob.
- Asset ID format: ASSET-{YYYY}-{NNNN} where NNNN is a zero-padded global counter. The counter must be read from a dedicated Airtable single-row config table (table name: asset_id_counter, field: last_used_id) and incremented atomically before write to prevent race conditions on concurrent events.
- Dedupe logic: before creating a new record or ID, query Airtable filtering by serial_number. If a match is found, route to an update branch rather than a create branch. Log the duplicate detection event to the error log table.
- Slack user ID must be resolved from the assigned_to linked record. The staff_members table must contain a slack_user_id field. If slack_user_id is null, fall back to posting in the #ops-assets channel with the assignee name in the message body.
- Confirm before build: Airtable base ID, table names, exact field names (case-sensitive), and the Slack workspace bot token with the chat:write and users:read OAuth scopes granted.
Reads maintenance interval data from each confirmed asset record and manages the complete reminder lifecycle. It creates recurring Google Calendar events for each scheduled maintenance date, sends a Slack reminder to the responsible person 48 hours before each due date, and fires an overdue loan alert to both the borrower and the operations manager when an asset's return_due_date passes without a status update in Airtable. This agent eliminates calendar juggling and manual Slack messages entirely and ensures 100 percent on-time reminder delivery regardless of coordinator availability.
// Input (maintenance scheduling branch) airtable.asset_id // string airtable.asset_name // string airtable.maintenance_interval // select: Monthly | Quarterly | Annually | Custom airtable.maintenance_due_date // ISO 8601 date airtable.assigned_to // linked record -> staff_member airtable.responsible_email // resolved from staff_member table // Input (overdue loan branch, daily poll) airtable.loan_status // select: On Loan | Returned | Overdue airtable.return_due_date // ISO 8601 date airtable.borrower_slack_id // from staff_member table airtable.ops_manager_slack_id // from config table, static value // Output (maintenance branch) google_calendar.event_id // created recurring event, stored in Airtable airtable.calendar_event_id // written back to record slack.reminder_message_ts // 48-hour pre-due Slack DM to responsible person // Output (overdue loan branch) airtable.loan_status // updated to 'Overdue' slack.overdue_alert_ts // DM to borrower + post to #ops-assets tagging ops manager
- Google Calendar API requires OAuth 2.0 with the https://www.googleapis.com/auth/calendar scope. The service account must be granted 'Make changes to events' permission on the shared ops calendar. The calendar ID must be stored in the config table, not hardcoded.
- Maintenance interval mapping: Monthly = recurrence every 30 days, Quarterly = every 90 days, Annually = every 365 days, Custom = read from airtable.custom_interval_days field. If custom_interval_days is null and interval is Custom, flag the record in error log and halt scheduling for that asset.
- The overdue loan poll must run as a scheduled trigger (cron: 07:00 daily) separate from the event-driven trigger. Filter Airtable for records where loan_status = 'On Loan' AND return_due_date < today. Update loan_status to 'Overdue' before sending alerts to avoid duplicate messages on repeat poll runs.
- Slack overdue alert must tag the ops manager by user ID, not just name, so the mention fires a notification. The ops_manager_slack_id must be stored in the config table and confirmed by the operations team before go-live.
- Google Workspace must be on Business Starter or above for Calendar API access. Confirm the service account has domain-wide delegation enabled if the build uses a shared calendar rather than per-user calendars.
- Confirm before build: Google Calendar ID, service account credentials JSON, Airtable field names for maintenance_interval and return_due_date, and the Slack user ID for the operations manager.
Pushes confirmed asset data from Airtable to Xero as a fixed-asset record, capturing purchase date, purchase cost, asset category, and depreciation method. When a disposal flag is set in Airtable, the agent updates the Xero record to reflect the write-off. This removes the manual finance handoff entirely, ensuring Xero stays aligned with the physical register without requiring the Finance Officer to re-enter data. The agent also writes the Xero fixed-asset ID back to the Airtable record to maintain a bidirectional audit trail.
// Input (new asset branch) airtable.asset_id // string, must exist before this agent fires airtable.asset_name // string -> Xero FixedAsset.Name airtable.asset_category // select -> mapped to Xero AssetType airtable.purchase_date // ISO 8601 -> Xero FixedAsset.PurchaseDate airtable.purchase_cost // number (USD) -> Xero FixedAsset.PurchasePrice airtable.depreciation_method // select: Straight-Line | Diminishing Value | None airtable.depreciation_rate // number, percentage, required if method is not None airtable.xero_account_code // string, e.g. '1500', confirmed by finance team // Input (disposal branch) airtable.xero_asset_id // Xero FixedAsset.AssetID, previously written back airtable.disposal_date // ISO 8601 airtable.disposal_reason // string: Sold | Written Off | Donated | Lost // Output (new asset branch) xero.fixed_asset_id // UUID returned by Xero API airtable.xero_asset_id // written back to Airtable record airtable.xero_sync_status // set to 'Synced' airtable.xero_sync_date // ISO 8601 timestamp of successful push // Output (disposal branch) xero.disposal_status // confirmed via Xero API response airtable.asset_status // updated to 'Disposed' airtable.xero_sync_status // updated to 'Disposed'
- Xero API authentication uses OAuth 2.0 with the offline_access and accounting.attachments scopes. The Xero tenant ID must be confirmed before build; a single-tenant connection is assumed. Token refresh must be handled automatically by the automation platform credential store.
- Asset type mapping must be agreed with the Finance Officer before the agent is activated. Example mapping: IT Hardware -> account code 1500, Tools -> 1510, Furniture -> 1520, Vehicle -> 1530. Store mappings in the config table in Airtable, not hardcoded in the workflow.
- Depreciation method must be one of the values Xero accepts via the Fixed Assets API: STRAIGHTLINE, DIMINISHINGVALUE100, or NOTDEPRECIATING. The Airtable select field values must map exactly to these strings. Include a translation step in the workflow.
- The agent must not fire until xero_account_code is populated in the Airtable record. If xero_account_code is null, write 'Pending Finance Review' to xero_sync_status and log to the error table. Do not retry automatically; require a human correction in Airtable.
- For the disposal branch, the agent must first confirm xero_asset_id exists on the record before calling the disposal endpoint. If xero_asset_id is null (asset was never synced), log the error and route a Slack alert to the ops manager rather than attempting a blind write.
- Confirm before build: Xero tenant ID, OAuth client ID and secret, account code list from Finance Officer, depreciation rate defaults per asset category, and whether the Xero organisation uses a trial balance period that would block historical date entries.
03End-to-end data flow
// ============================================================
// TRIGGER: Airtable asset register event
// ============================================================
EVENT: airtable.webhook -> table='assets', event_type IN ['create','update']
PAYLOAD: {
record_id, // Airtable internal row ID
asset_name, // string
serial_number, // string
purchase_date, // ISO 8601
purchase_cost, // number USD
asset_category, // select
assigned_to, // linked record ID
maintenance_interval, // select
maintenance_due_date, // ISO 8601
loan_status, // select
return_due_date, // ISO 8601
disposal_flag, // boolean
xero_account_code // string
}
// ============================================================
// AGENT HANDOFF 1: ASSET INTAKE AGENT
// ============================================================
STEP 1: validate required fields
IF asset_name IS NULL OR serial_number IS NULL
OR purchase_date IS NULL OR purchase_cost IS NULL
-> write intake_status='Validation Failed'
-> log to error_log table {record_id, missing_fields, timestamp}
-> HALT
STEP 2: deduplicate by serial_number
QUERY airtable.assets WHERE serial_number = payload.serial_number
IF match found AND record_id != existing_record_id
-> write intake_status='Duplicate Detected'
-> log to error_log {record_id, duplicate_of: existing_record_id}
-> HALT
STEP 3: generate asset_id if null
READ airtable.asset_id_counter.last_used_id
new_id = last_used_id + 1
asset_id = 'ASSET-' + YEAR(NOW()) + '-' + LPAD(new_id, 4, '0')
WRITE airtable.asset_id_counter.last_used_id = new_id // atomic
WRITE airtable.assets[record_id].asset_id = asset_id
STEP 4: generate QR code
REQUEST GET https://api.qrserver.com/v1/create-qr-code/
?data={airtable_record_url}&size=200x200&format=png
RESPONSE: qr_code_url (public PNG URL)
WRITE airtable.assets[record_id].qr_code_url = qr_code_url
STEP 5: resolve assignee Slack ID
QUERY airtable.staff_members WHERE id = assigned_to
assignee_slack_id = staff_member.slack_user_id
IF assignee_slack_id IS NULL
-> fallback_channel = '#ops-assets'
ELSE
-> dm_channel = assignee_slack_id
STEP 6: send Slack assignment notification
POST slack.chat.postMessage
channel: dm_channel OR fallback_channel
text: 'Asset {asset_name} ({asset_id}) has been assigned to you.
QR label: {qr_code_url}. Check the register for care instructions.'
WRITE airtable.assets[record_id].intake_status = 'Confirmed'
WRITE airtable.assets[record_id].slack_message_ts = response.ts
// ============================================================
// AGENT HANDOFF 2: MAINTENANCE SCHEDULER AGENT
// ============================================================
TRIGGER: intake_status changed to 'Confirmed' OR maintenance_due_date updated
STEP 7: map maintenance interval to recurrence rule
SWITCH maintenance_interval
'Monthly' -> RRULE:FREQ=MONTHLY
'Quarterly' -> RRULE:FREQ=MONTHLY;INTERVAL=3
'Annually' -> RRULE:FREQ=YEARLY
'Custom' -> RRULE:FREQ=DAILY;INTERVAL={custom_interval_days}
DEFAULT -> log error, HALT scheduling for this record
STEP 8: create Google Calendar event
POST google_calendar.events.insert
calendarId: {ops_calendar_id from config}
summary: 'Maintenance Due: {asset_name} ({asset_id})'
start: maintenance_due_date
recurrence: [RRULE from step 7]
attendees: [{responsible_email}]
RESPONSE: google_calendar.event_id
WRITE airtable.assets[record_id].calendar_event_id = event_id
STEP 9: 48-hour pre-due Slack reminder (scheduled sub-workflow)
SCHEDULE at maintenance_due_date - 48h
POST slack.chat.postMessage
channel: assignee_slack_id OR '#ops-assets'
text: 'Reminder: {asset_name} ({asset_id}) is due for maintenance in 48 hours.'
// ============================================================
// AGENT HANDOFF 2b: OVERDUE LOAN BRANCH (daily scheduled poll)
// ============================================================
SCHEDULE: cron 07:00 daily
QUERY airtable.assets WHERE loan_status='On Loan' AND return_due_date < TODAY()
FOR EACH overdue_record:
WRITE airtable.assets[record_id].loan_status = 'Overdue'
POST slack.chat.postMessage to borrower_slack_id:
'Your loan of {asset_name} ({asset_id}) was due back on {return_due_date}.
Please return it or update the register.'
POST slack.chat.postMessage to #ops-assets:
'@{ops_manager} Overdue loan alert: {asset_name} ({asset_id}).
Borrower: {assigned_to}. Due: {return_due_date}.'
// ============================================================
// AGENT HANDOFF 3: FINANCE SYNC AGENT
// ============================================================
TRIGGER A: intake_status = 'Confirmed' AND xero_sync_status IS NULL
TRIGGER B: disposal_flag = TRUE
// Branch A: new fixed-asset record
STEP 10: validate xero_account_code
IF xero_account_code IS NULL
-> WRITE xero_sync_status = 'Pending Finance Review'
-> log to error_log {record_id, reason: 'Missing xero_account_code'}
-> HALT
STEP 11: map depreciation method
SWITCH depreciation_method
'Straight-Line' -> xero_method = 'STRAIGHTLINE'
'Diminishing Value' -> xero_method = 'DIMINISHINGVALUE100'
'None' -> xero_method = 'NOTDEPRECIATING'
STEP 12: POST to Xero Fixed Assets API
POST https://api.xero.com/assets.xro/1.0/Assets
AssetName: asset_name
AssetNumber: asset_id
PurchaseDate: purchase_date
PurchasePrice: purchase_cost
AssetStatus: 'REGISTERED'
BookDepreciationSetting: {
DepreciationMethod: xero_method,
AveragingMethod: 'ActualDays',
DepreciationRate: depreciation_rate,
DepreciationCalculationMethod: 'None'
}
FixedAssetAccountCode: xero_account_code
RESPONSE: xero.AssetID (UUID)
WRITE airtable.assets[record_id].xero_asset_id = xero.AssetID
WRITE airtable.assets[record_id].xero_sync_status = 'Synced'
WRITE airtable.assets[record_id].xero_sync_date = NOW()
// Branch B: disposal / write-off
STEP 13: confirm xero_asset_id exists
IF xero_asset_id IS NULL
-> log error, POST Slack alert to #ops-assets, HALT
STEP 14: POST disposal to Xero
POST https://api.xero.com/assets.xro/1.0/Assets/{xero_asset_id}/Dispose
DisposalDate: disposal_date
DisposalType: disposal_reason mapped to Xero enum
WRITE airtable.assets[record_id].asset_status = 'Disposed'
WRITE airtable.assets[record_id].xero_sync_status = 'Disposed'
// ============================================================
// SCHEDULED AUDIT REPORT (monthly, 09:00 first Monday)
// ============================================================
QUERY airtable.assets WHERE last_status_update < DATE_SUB(NOW(), INTERVAL 30 DAY)
BUILD summary: total_assets, active, on_loan, overdue, disposed, stale_records[]
POST slack.chat.postMessage to #ops-assets:
'Monthly Asset Audit Report: {summary}. Stale records attached for review.'
// END OF FLOW04Recommended build stack
More documents for this process
Every document generated for Asset & Equipment Tracking.