Table of Contents
- 1 The Business Situation
- 2 The Existing Process
- 2.1 Current operational problems
- 2.2 Practical business effects
- 3 What the New System Needed to Do
- 4 Implementation Approaches Considered
- 4.1 Improving the existing spreadsheet
- 4.2 Using a Microsoft-only productivity workflow
- 4.3 Purchasing dedicated software
- 4.4 Building a custom application
- 4.5 Selected approach
- 5 The Selected Solution
- 6 System Architecture and Data Flow
- 7 Data Structure
- 7.1 Asset master
- 7.2 Asset transactions
- 7.3 License and workflow records
- 8 Workflow Statuses and Ownership
- 8.1 Physical asset statuses
- 8.2 Task statuses
- 9 Step-by-Step Implementation
- 9.1 Step 1: Prepare the Accounts and Permissions
- 9.2 Step 2: Build the Intake
- 9.3 Step 3: Create the System of Record
- 9.4 Step 4: Connect the Tools
- 9.5 Step 5: Build the Core Automation
- 9.6 Step 6: Add Approvals, Reminders, and Escalations
- 9.7 Step 7: Add Documents and File Management
- 9.8 Step 8: Add Reporting and Operational Views
- 9.9 Step 9: Add Security and Governance Controls
- 9.10 Step 10: Deploy and Test
- 10 Code and Configuration
- 10.1 Microsoft Graph authentication and requests
- 10.2 Prepare the Code node input
- 10.3 Complete n8n Code node for license comparison
- 10.4 Route and write the comparison output
- 10.5 Slack configuration
- 10.6 Retry and deployment configuration
- 11 Failure Handling and Operational Reliability
- 12 A Complete Example
- 13 Implementation Cost
- 14 Estimated Time and Cost Savings
- 15 Adding AI to the Automation
- 15.1 The Recommended AI Enhancement
- 15.2 Benefits of the AI Enhancement
- 15.3 What Remains Rule-Based or Human-Controlled
- 15.4 Estimating the Additional Value of AI
- 16 Testing Checklist
- 17 Ongoing Maintenance
- 18 When to Move to Dedicated Software
- 19 Implementation Checklist
The Business Situation
A representative 120-person engineering consultancy needed a more reliable way to manage laptops, phones, monitors, docks, shared equipment, warranties, software licenses, maintenance, and disposal. Employees worked from a mixture of offices and remote locations, so equipment frequently moved between people and sites.
The company had approximately 463 physical assets: 150 laptops, 65 mobile phones, 230 monitors and docks, and 18 pieces of shared technical equipment. Microsoft Entra ID contained about 560 paid license assignments across 16 software SKUs.
The IT function consisted of an IT manager and an IT support coordinator. Finance involved a controller and a procurement analyst. Department managers confirmed employee assignments and license requirements. The process generated about 28 equipment movement or maintenance events and 12 employee account changes each month.
Existing tools included Microsoft Entra ID for user identities and Microsoft license assignments, Google Workspace for forms and spreadsheets, and Slack for internal communication. No individual tool provided a dependable asset register across physical equipment, software assignments, warranty information, maintenance history, and disposal evidence.
The immediate problem was not simply that data lived in spreadsheets. The larger issue was that the spreadsheets, identity platform, finance records, and day-to-day communications were disconnected. Staff could not reliably answer basic questions such as who had a particular laptop, which licenses were assigned to disabled users, which warranties were approaching expiry, or whether a retired device had received finance and data-disposal approval.
Note: This case study is provided as a representative example of the types of AI integration and digital transformation solutions Intelligex designs and delivers. Actual engagements are tailored to each client’s goals, constraints, existing systems, timeline, and available resources, so the approach, tools, and outcomes may vary.
The Existing Process
The original process depended on email, Slack messages, several spreadsheets, and individual staff knowledge.
- Finance approved a purchase and stored the supplier invoice in its normal accounting records.
- When equipment arrived, an IT employee entered some of the purchase information into an equipment spreadsheet. Serial numbers and warranty dates were sometimes added later.
- Equipment assignment was communicated through Slack or email. The asset spreadsheet was updated when the IT employee remembered to do so.
- Check-ins, temporary loans, maintenance, and location changes were recorded inconsistently. Some appeared in the spreadsheet, while others existed only in messages.
- Employee departures generated a separate checklist. IT reviewed Entra ID manually and attempted to identify software that should be removed.
- Before a software renewal, Finance requested a current user count. IT exported users or inspected assignments and compared the information manually.
- Retired equipment was placed in storage. Disposal approval, data-wipe evidence, and finance write-off information were not always linked to the original asset record.
Current operational problems
- Duplicate and incomplete asset records
- Missing serial numbers and warranty dates
- Assignments recorded in messages but not in the register
- License exports compared manually
- No consistent renewal or removal task
- Disposal evidence stored separately
- Limited audit history
Practical business effects
- IT spent time locating equipment and confirming custody.
- Finance could not confidently reconcile purchases with assets.
- Unused software could remain assigned until renewal.
- Managers received inconsistent follow-up requests.
- Warranty and return deadlines could be missed.
- Responsibility for overdue actions was unclear.
- Reports required repeated manual reconciliation.
Spreadsheet maintenance was also dependent on one IT employee who understood the existing columns and naming conventions. If that employee was unavailable, other staff hesitated to update the register because they could not tell which fields were authoritative.
There was no reliable idempotency control. Idempotency means that processing the same event more than once does not create multiple records or tasks. A repeated export, form resubmission, or retried automation could therefore create duplicates unless an employee noticed and removed them.
Microsoft Entra ID identified users and assigned Microsoft licenses, but it did not serve as proof of physical custody. A registered device record could indicate that a device interacted with the tenant, but it did not replace a signed check-out, serial number verification, or physical return process.
What the New System Needed to Do
The team defined the operating requirements before selecting the implementation tools.
| Requirement | Required behavior | Control |
|---|---|---|
| Equipment intake | Capture category, serial number, purchase data, condition, location, warranty, and supporting documents. | Required fields and duplicate checks |
| Unique identification | Assign one stable asset ID to every tracked item. | Automation-generated ID and protected columns |
| Custody tracking | Record check-out, check-in, transfer, location, custodian, and expected return date. | Append-only transaction history |
| Maintenance | Record faults, repair status, provider, cost, next maintenance date, and return to service. | Open maintenance queue and reminders |
| License reconciliation | Compare Entra users, sign-in activity, and assigned licenses on a schedule. | Read-only Graph API integration |
| Unused-license review | Flag disabled accounts, prolonged inactivity, missing sign-in information, and unmapped SKUs. | Human review before license removal |
| Renewal control | Create tasks before license and support renewal dates. | Lead-time rules and finance ownership |
| Disposal | Require IT data-handling confirmation and finance approval before final disposal. | Sequential approval evidence |
| Documents | Link invoices, warranty documents, repair evidence, approval records, and disposal certificates. | Restricted Google Drive folders |
| Notifications | Send Slack messages when staff need to act, without making Slack the official record. | Task IDs and links back to the register |
| Reporting | Show assets by status, owner, location, condition, warranty, task status, and automation health. | Protected dashboard sheets and filtered views |
| Exception handling | Record failed runs, invalid data, retry counts, and recovery ownership. | Errors sheet and manual-review queue |
| Permissions | Separate intake, operational updates, approval, reporting, and administration. | Least-privilege access |
| Manual override | Allow authorized staff to correct data without deleting history. | Correction transaction and reason |
The company also decided that automation would not remove licenses, approve disposal, release payments, or decide whether an employee still needed software. It would identify candidates, gather evidence, assign work, and verify the resulting state.
Implementation Approaches Considered
| Approach | Connected tools | Effort | Customization | Primary limitation |
|---|---|---|---|---|
| Improve the existing spreadsheet | Google Sheets only | Low | Moderate | No dependable identity or notification integration |
| Microsoft productivity-suite workflow | Entra ID, Microsoft Lists, Power Automate, Teams | Moderate | Moderate | Would duplicate the company’s established Google and Slack workflows |
| Dedicated IT asset and software management platform | Vendor platform, identity provider, finance data | Moderate to high | Platform-dependent | Higher recurring commitment and more migration work |
| Integration-led asset register | Entra ID, Google Sheets, n8n, Google Drive, Slack | Moderate | High | Requires disciplined ownership and spreadsheet governance |
| Custom application and database | Web application, PostgreSQL, APIs, identity provider | High | Very high | More engineering and support than the current volume justified |
Improving the existing spreadsheet
A cleaned-up spreadsheet with validation and protected ranges would improve consistency. It would not address weekly Entra comparisons, renewal task creation, Slack reminders, or execution logging. Staff would still spend time exporting, filtering, and following up.
Using a Microsoft-only productivity workflow
Microsoft Lists and Power Automate could support this process, particularly in an organization already standardized on Microsoft 365 collaboration tools. In this scenario, operational staff already worked in Google Workspace and Slack. Replacing those tools solely for the asset register would increase adoption effort and create duplicate working practices.
Purchasing dedicated software
A dedicated IT asset management or software asset management platform would provide stronger inventory controls, richer discovery, vendor support, and potentially deeper software-usage data. It remained a valid future option, but the company first needed a controlled operating process and clean baseline data. Its current volume did not require a full enterprise platform.
Building a custom application
A custom application with a relational database would provide hard uniqueness constraints, transactional updates, and a tailored interface. It would also require application hosting, authentication, user-interface development, backup administration, and ongoing software maintenance. That was disproportionate for the initial transaction volume.
Selected approach
The integration-led register was selected because it retained the tools employees already used, introduced a clear system of record, connected identity and license data through documented APIs, and could later be migrated to a dedicated database or asset platform.
The Selected Solution
The solution used Google Forms for controlled internal intake, Google Sheets as the operational system of record, Google Drive for documents, n8n as the automation layer, Microsoft Entra ID as the user and license source, and Slack for operational notifications.
| Tool | Responsibility | Authoritative for |
|---|---|---|
| Microsoft Entra ID | Provide user status, user identifiers, assigned license SKU IDs, and available sign-in activity through Microsoft Graph. | Identity and current Microsoft license assignment |
| Google Forms | Collect equipment receipt, custody changes, maintenance events, and approval responses. | Submitted event evidence, not current asset state |
| Google Sheets | Store asset master records, transaction history, license catalog, snapshots, tasks, run logs, and operational reports. | Current physical asset state and workflow records |
| n8n | Validate events, generate IDs, read Graph data, compare records, create tasks, update statuses, and coordinate retries. | Automation execution and integration logic |
| Google Drive | Store invoices, warranty files, repair evidence, and disposal documentation. | Supporting documents |
| Slack | Notify owners, approvers, and administrators about work requiring attention. | Communication only |
| Optional approved AI service | Summarize maintenance notes and suggest a review category. | Advisory output only |
Google Sheets remained suitable because the record volume was modest, the team already understood the platform, and only a small number of trained users needed update access. Append-only transaction and snapshot sheets preserved history, while protected master sheets reduced accidental changes.
n8n was selected because it could coordinate scheduled Microsoft Graph requests, Google Sheets operations, Google Drive actions, Slack notifications, branching, retries, and JavaScript transformations in one automation environment.
The following manual decisions remained in place:
- A manager confirmed whether a user still needed a paid license.
- IT performed license removal in Microsoft Entra ID.
- IT verified physical serial numbers and equipment condition.
- IT confirmed data backup, wipe, or destruction requirements.
- Finance approved disposal and accounting treatment.
- Finance and department owners approved material renewals.
- Authorized staff resolved conflicting or incomplete records.
System Architecture and Data Flow
- Intake: Internal Google Forms linked to staging sheets
- System of record: A controlled Google Sheets workbook
- Automation layer: n8n workflows and JavaScript Code nodes
- Document storage: Restricted Google Drive folders
- Notifications: Slack channels and direct messages where permitted
- Reporting: Google Sheets dashboard tabs, pivot tables, and filtered views
- AI layer: Optional maintenance-note classification with human review
- An internal user submits an event. A Google Form receives equipment, assignment, return, maintenance, or approval information. Google Forms writes the response to its linked staging sheet. The form response row number becomes the source event identifier.
- n8n detects the new row. A Google Sheets trigger or scheduled polling workflow reads unprocessed rows. It validates required fields, normalizes serial numbers and email addresses, and checks whether the source event has already been processed.
- The automation creates or updates the asset record. A new asset receives an ID such as
AST-2026-00184. A movement event updates the current custodian, location, status, condition, and last transaction ID. - A transaction record is appended. Every state-changing event is written to the append-only
Asset_Transactionssheet. The currentAssetsrow and historical transaction remain linked by Asset ID. - A document folder is created or located. n8n creates a Drive folder for a new asset and records its folder ID and link. Supporting files are copied or moved according to the organization’s Drive policy.
- A Slack notification is sent when action is required. The message contains a task ID, summary, due date, and controlled link to the task or approval form. Slack’s returned channel and message timestamp are stored in the task row.
- A scheduled license workflow reads Entra ID. n8n authenticates to Microsoft Graph using an Entra application registration. It retrieves users, account status, assigned license identifiers, sign-in activity where available, and subscribed SKU metadata.
- The workflow compares assignments. A Code node maps license assignments to the internal catalog. It identifies disabled users, prolonged inactivity, missing activity data, unmapped SKUs, and valid exemptions.
- Review tasks are created idempotently. Before creating a task, n8n searches the Tasks sheet for the deterministic idempotency key. Existing tasks are updated instead of duplicated.
- Humans review and act. Managers and IT confirm license needs. IT removes approved licenses in Entra ID. Finance and department owners decide renewals. Disposal follows sequential IT and finance approval.
- Subsequent runs verify completion. If a license assignment no longer appears in Entra ID, the corresponding removal task can move to
Verified Removed. Failed records remain in an exception queue until corrected and replayed.
If a source event fails validation, n8n does not partially update the asset master. It marks the staging response as Needs Information, records the error, and creates a manual-review task. If an external call fails after a local record has been written, the record remains in Partial Failure until the missing action is retried.
Data Structure
The Google Sheets workbook contained current-state tables, append-only history, reference data, operational tasks, and automation logs. Each tab had a fixed header row, protected system columns, and named data ranges.
Asset master
| Field | Type | Required | Source | Purpose and validation |
|---|---|---|---|---|
| Asset_ID | Text | Yes | n8n | Unique immutable ID matching AST-YYYY-NNNNN. |
| Category | Controlled text | Yes | Intake | Laptop, Phone, Monitor, Dock, Shared Equipment, or Other. |
| Manufacturer | Text | Yes | Intake | Normalized display value. |
| Model | Text | Yes | Intake | Supplier or manufacturer model. |
| Serial_Number | Text | Conditional | Intake | Required for serialized equipment; normalized to uppercase for duplicate checks. |
| Asset_Tag | Text | Yes | IT | Physical tag value; unique where tags are used. |
| Entra_Device_ID | Text | No | IT or integration | Optional reference to a corresponding Entra device object. |
| Status | Controlled text | Yes | Automation | Received, Available, Assigned, Checked Out, Maintenance, Pending Disposal, Disposed, Lost, or Exception. |
| Location | Controlled text | Yes | Intake or transaction | Approved office, remote, storage, repair provider, or disposal location. |
| Custodian_Email | Conditional | Transaction | Required when status is Assigned or Checked Out. | |
| Condition | Controlled text | Yes | Intake or transaction | New, Good, Fair, Damaged, Unserviceable, or Unknown. |
| Purchase_Date | Date | Yes | Finance or intake | Cannot be materially later than the receipt date without review. |
| Supplier | Text | Yes | Finance or intake | Supplier name used for reporting and warranty support. |
| Purchase_Cost | Currency | Conditional | Finance | Restricted to Finance and authorized IT administrators. |
| PO_Invoice_Ref | Text | Conditional | Finance | Reference to the purchasing or accounting record. |
| Warranty_Expiry | Date | No | Intake | Used for 90-day, 60-day, and expired-warranty views. |
| Checkout_Due | Date | Conditional | Transaction | Required for temporary check-outs. |
| Maintenance_Due | Date | No | IT | Next preventive maintenance or inspection date. |
| Disposal_Status | Controlled text | Yes | Automation | Not Requested, Pending IT, Pending Finance, Approved, Rejected, or Completed. |
| Disposal_Approval_ID | Text | Conditional | Approval workflow | Links the asset to approval evidence. |
| Document_Folder_ID | Text | Yes | n8n | Google Drive folder identifier. |
| Document_Link | URL | Yes | n8n | Controlled link to supporting documentation. |
| Last_Transaction_ID | Text | Yes | n8n | References the latest accepted state-changing event. |
| Created_At | Date and time | Yes | n8n | Immutable creation timestamp. |
| Updated_At | Date and time | Yes | n8n | Timestamp of the latest accepted change. |
| Automation_Status | Controlled text | Yes | n8n | Pending, Complete, Needs Information, Partial Failure, or Manual Review. |
| Last_Automation_Run | Date and time | No | n8n | Most recent workflow attempt. |
| Retry_Count | Integer | Yes | n8n | Starts at zero and increments after recoverable failure. |
| Error_Message | Text | No | n8n | Sanitized operational error, without credentials or sensitive payloads. |
| Notes | Text | No | Authorized user | Short operational note; corrections require a transaction record. |
Asset transactions
| Field | Type | Purpose |
|---|---|---|
| Transaction_ID | Text | Unique event ID generated from the response row and event date. |
| Source_Event_ID | Text | Idempotency key derived from the form response source. |
| Asset_ID | Text | Foreign-reference value linking to the Assets sheet. |
| Event_Type | Controlled text | Receive, Assign, Transfer, Check Out, Check In, Maintenance Open, Maintenance Complete, Retire Request, Disposal Approval, Dispose, Lost, or Correction. |
| Event_Time | Date and time | When the event occurred. |
| Requester_Email | Authenticated form respondent. | |
| From_Custodian | Previous custodian where applicable. | |
| To_Custodian | New custodian where applicable. | |
| From_Location | Text | Previous location. |
| To_Location | Text | New location. |
| Condition | Controlled text | Condition observed during the event. |
| Due_Date | Date | Return, maintenance, approval, or disposal deadline. |
| Document_Link | URL | Evidence related to the event. |
| Automation_Status | Controlled text | Processing state and recovery control. |
License and workflow records
| Entity | Important fields | Relationship |
|---|---|---|
| Licenses | SKU_ID, SKU_Part_Number, Display_Name, Purchased_Units, Effective_Monthly_Cost, Renewal_Date, Notice_Date, Owner_Email, Review_Enabled, Status | One catalog row can relate to many assignment snapshots and tasks. |
| User_License_Snapshot | Snapshot_Key, Run_ID, Snapshot_Date, User_ID, User_Email, Account_Enabled, Last_Successful_Sign_In, SKU_ID, Candidate, Candidate_Reason, Exempt_Until | Append-only evidence of an assignment observed during a Graph run. |
| Tasks | Task_ID, Idempotency_Key, Task_Type, Asset_ID, User_ID, SKU_ID, Owner_Email, Status, Due_Date, Approval_Status, Slack_Message_TS, Error_Message | Can reference an asset, user, license, transaction, or combination. |
| Exemptions | User_ID, SKU_ID, Exempt_Until, Reason, Approved_By, Approved_At | Suppresses inactivity candidates temporarily, but does not suppress disabled-account review. |
| Runs | Run_ID, Workflow_Name, Started_At, Completed_At, Records_Read, Records_Written, Errors, Status | Provides one audit summary for each automation execution. |
| Errors | Error_ID, Run_ID, Source_Event_ID, Record_ID, Step, Error_Type, Retry_Count, Payload_Reference, Status, Owner | Acts as a controlled dead-letter register for unrecovered events. |
| Lookups | Lookup_Type, Lookup_Key, Display_Value, Active, Owner_Email, Delegate_Email | Stores locations, categories, conditions, approvers, and assignment rules. |
Google Sheets does not enforce relational foreign keys or unique constraints. The implementation compensated by protecting ID columns, performing pre-write lookups, limiting workflow concurrency for asset intake, and running scheduled reconciliation checks. If transaction volume or concurrency increased substantially, these controls would need to move to a relational database or dedicated platform.
Workflow Statuses and Ownership
Physical asset statuses
| Status | Meaning | Owner | Entry condition | Exit condition | Reminder or escalation |
|---|---|---|---|---|---|
| Received | Equipment recorded but not yet prepared. | IT support | Validated receipt form | Inspection and tagging complete | Reminder after two business days |
| Available | Ready for assignment or loan. | IT support | Preparation complete or checked in | Assignment, check-out, maintenance, or disposal request | No routine reminder |
| Assigned | Issued to an employee without a short return date. | Custodian and IT | Approved assignment transaction | Transfer, check-in, maintenance, loss, or retirement | Quarterly custody confirmation where required |
| Checked Out | Temporary loan with an expected return date. | Custodian | Check-out transaction with due date | Check-in or approved extension | Three days before due date, on due date, then escalation after three days |
| Maintenance | Unavailable while inspected or repaired. | IT support | Maintenance Open event | Maintenance Complete, retirement, or loss decision | Reminder at target date and escalation after five days |
| Pending Disposal | Retirement requested but not complete. | IT manager, then Finance | Retire Request accepted | Approval, rejection, or return to service | Approval reminders every three days |
| Disposed | Final disposal completed and evidenced. | Finance record owner | Approvals and disposal evidence present | Closed state; correction only | No reminder |
| Lost | Equipment cannot be located. | IT manager | Lost or stolen report | Recovered, insurance closure, or approved write-off | Immediate security and management escalation |
| Exception | Current state conflicts with required data or history. | IT manager | Validation or reconciliation failure | Correction transaction accepted | Daily until assigned |
Task statuses
| Status | Meaning | Owner action | Moves forward when | Moves backward when |
|---|---|---|---|---|
| New | Automation created the task. | Review assignment and evidence. | Owner acknowledges or workflow assigns an approver. | Not applicable |
| Awaiting Owner | Business owner must confirm need or action. | Approve, reject, request information, or reassign. | A valid response is recorded. | Submitted information is incomplete. |
| Awaiting IT | IT must perform a technical action. | Remove a license, inspect equipment, wipe a device, or update evidence. | Technical action and evidence are recorded. | Approval is withdrawn or action fails. |
| Awaiting Finance | Finance must decide renewal, write-off, or disposal. | Approve, reject, or return for information. | Decision is captured from an authorized user. | Supporting cost or ownership data changes. |
| Blocked | A dependency prevents progress. | Record blocker and expected resolution date. | Dependency is removed. | Not applicable |
| Approved for Removal | Human approval exists for license removal. | IT removes the assignment in Entra ID. | Next reconciliation confirms absence. | Manager reverses approval before removal. |
| Verified Removed | Entra no longer reports the assignment. | No further action. | Automation closes the task. | Assignment reappears in a later run. |
| Renewed | Renewal decision and evidence are recorded. | Update new renewal and notice dates. | Catalog is updated. | Evidence is incomplete. |
| Rejected | The proposed action was declined. | Provide reason and any exemption expiry. | Task closes or returns for review. | New evidence requires reconsideration. |
| Closed | Required action and evidence are complete. | None. | Closure checks pass. | Reconciliation detects a conflict. |
| Automation Error | A technical step failed after retries. | Administrator corrects and replays the step. | Recovery succeeds. | Retry fails again. |
A rejected disposal request returned the asset to its prior operational status unless IT had already identified a security reason that prevented reuse. A license review dismissed because of approved long-term leave required an exemption expiry date. When the exemption expired, the next scheduled comparison could create or reopen the review task.
Step-by-Step Implementation
Step 1: Prepare the Accounts and Permissions
The implementation used separate administrator and operational accounts. Individual credentials were not embedded in workflow nodes.
- Create the production workbook. Place it in a controlled Google Workspace location owned by the organization rather than an individual employee. Limit editor access to the IT manager, IT support coordinator, controller, procurement analyst, and the automation identity.
- Create a test workbook. Copy the tab structure without real employee, device, cost, or supplier data. Use synthetic users and assets during development.
- Prepare Google credentials for n8n. Use a dedicated Google OAuth connection or service account supported by the organization’s n8n deployment. Share only the required workbook and Drive root folder with that identity.
- Create a Microsoft Entra application registration. Use a dedicated application for the asset workflow. For unattended scheduling, configure client-credentials authentication. A certificate is preferable where the n8n deployment and organizational controls support it; otherwise store a client secret in n8n’s encrypted credential store.
- Grant Microsoft Graph application permissions. Use
User.Read.Allfor user records,LicenseAssignment.Read.Allfor license assignment information, andAuditLog.Read.Allwhen last sign-in activity is required. Administrative consent is required. Confirm current Microsoft documentation before deployment because permission requirements can change. - Confirm sign-in activity availability. The
signInActivityproperty can depend on tenant licensing, permissions, data retention, and user history. Missing activity must produce a review state, not an assumption that a license is unused. - Create a Slack application. Grant the minimum bot permission required to post messages, normally
chat:write. Add the app only to the operational channels it needs. Use a private test channel during development. - Configure n8n roles. Limit workflow editing and credential use to automation administrators. Operational users can receive task links without receiving n8n editor access.
- Create backup owners. Assign a primary system owner in IT and a backup owner with access to the workbook, n8n project, Entra app registration, Slack app configuration, and recovery documentation.
- Record credential metadata. Store owner, creation date, expiry date, rotation date, and purpose. Never place client secrets, Slack tokens, or Google private keys in Sheets.
The Entra application was read-only. It could retrieve user and license data but could not assign or remove licenses. This permission boundary ensured that license removal remained a deliberate human-controlled action.
Step 2: Build the Intake
Four internal Google Forms were used so that each form remained short and had clear permissions.
| Form | Important fields | Primary users |
|---|---|---|
| Asset Receipt | Category, manufacturer, model, serial number, asset tag, purchase date, supplier, cost, invoice reference, warranty expiry, initial location, condition, document upload | IT and procurement |
| Asset Movement | Event type, asset ID, from location, to location, new custodian, condition, due date, notes, evidence | IT and authorized office coordinators |
| Maintenance and Retirement | Asset ID, issue type, condition, description, provider, target date, estimated cost, retirement reason, supporting document | IT |
| Approval Decision | Task ID, decision, comments, proposed expiry or renewal date, evidence reference | Authorized managers, IT manager, and Finance |
Each form collected the respondent’s authenticated organization email. Access was restricted to approved internal users where the Google Workspace configuration supported it. The confirmation message stated that submission did not itself approve a purchase, disposal, license removal, or accounting treatment.
Conditional sections reduced incomplete submissions. For example:
Assignrequired the new custodian email.Check Outrequired both custodian and return date.Check Inrequired the observed condition.Maintenance Openrequired issue description and target date.Retire Requestrequired retirement reason and data-bearing-device confirmation.Approve Renewalrequired a new renewal date and evidence reference.Rejectrequired a reason.
The source event ID combined the form identity and response row number, such as MOVE:1842. n8n searched Asset_Transactions.Source_Event_ID before processing. If the key already existed, the workflow returned the existing transaction ID and did not create another event.
Serial numbers were normalized by trimming whitespace and converting letters to uppercase. A new receipt was held in Manual Review when the same serial number or asset tag already appeared on another active asset.
File uploads were restricted by organization policy. Large files, unsupported file types, or inaccessible links moved the event to Needs Information. A missing document did not prevent initial receipt when the document was optional, but it generated a document-completion task.
The forms included a short privacy notice explaining that asset custody, work contact information, and workflow activity would be recorded for operational, security, finance, and audit purposes. Free-text fields instructed users not to enter passwords, authentication codes, personal health information, or unrelated employee information.
Step 3: Create the System of Record
The workbook used fixed tab names so that n8n mappings did not depend on user-created sheet names:
Assets
Asset_Transactions
Licenses
User_License_Snapshot
Tasks
Exemptions
Runs
Errors
Lookups
Dashboard
Form_Asset_Receipt
Form_Asset_Movement
Form_Maintenance
Form_Approvals
These names were created in the production and test workbooks. The first row of every operational sheet contained protected headers. n8n was configured against header names rather than column positions where the node supported it.
The exact Assets header order was:
A Asset_ID
B Category
C Manufacturer
D Model
E Serial_Number
F Asset_Tag
G Entra_Device_ID
H Status
I Location
J Custodian_Email
K Condition
L Purchase_Date
M Supplier
N Purchase_Cost
O PO_Invoice_Ref
P Warranty_Expiry
Q Checkout_Due
R Maintenance_Due
S Disposal_Status
T Disposal_Approval_ID
U Document_Folder_ID
V Document_Link
W Last_Transaction_ID
X Created_At
Y Updated_At
Z Automation_Status
AA Last_Automation_Run
AB Retry_Count
AC Error_Message
AD Notes
AE Warranty_Flag
The exact Tasks header order was:
A Task_ID
B Idempotency_Key
C Task_Type
D Asset_ID
E User_ID
F User_Email
G SKU_ID
H Title
I Reason
J Owner_Email
K Priority
L Status
M Due_Date
N Approval_Status
O Slack_Channel
P Slack_Message_TS
Q External_Evidence_ID
R Created_At
S Updated_At
T Closed_At
U Automation_Status
V Retry_Count
W Error_Message
X Last_Automation_Run
Y Notes
Data validation used the Lookups sheet for categories, conditions, locations, statuses, task types, and active owners. System-managed columns such as IDs, timestamps, retry counters, and Slack message timestamps were protected from normal editors.
Asset IDs followed AST-YYYY-NNNNN. For form-created assets, the numeric suffix was based on the unique receipt response row. Existing assets imported during migration received IDs through a controlled one-time n8n workflow. Transaction IDs used TXN-YYYYMMDD-NNNNN.
Readable task IDs used the n8n execution ID and item index. A separate deterministic Idempotency_Key prevented duplicate business tasks. Examples included:
LICREV:USER_GUID:SKU_GUID:DISABLED_ACCOUNTRENEW:SKU_GUID:2026-11-30RETURN:AST-2026-00184:2026-08-15DISPOSAL:AST-2026-00184
The Assets sheet represented current state. Historical changes were never reconstructed solely from overwritten asset cells. Every accepted change also created an append-only transaction record.
The warranty flag in AE2 used this formula and was copied down:
=IF($A2="","",IF($P2="","Unknown",IF($P2<TODAY(),"Expired",IF($P2<=TODAY()+60,"Due in 60 days","Current"))))
The formula does not decide whether equipment should be replaced. It supports a filtered warranty-review view.
Step 4: Connect the Tools
| Source | Destination | Trigger and authentication | Returned identifier | Failure behavior |
|---|---|---|---|---|
| Google Forms | Google Sheets staging tabs | Native linked response destination | Response row and timestamp | Incomplete responses remain unprocessed |
| Google Sheets | n8n | Row-added trigger or scheduled polling; Google OAuth or service account | Source event ID | Retry, then Errors sheet |
| n8n | Microsoft Graph | Schedule trigger; Entra client credentials | User IDs, SKU IDs, and Graph response metadata | Honor throttling, retry, do not replace the last successful snapshot |
| n8n | Google Sheets | Google credential with workbook-only access | Created or updated row reference | Mark partial failure and reconcile |
| n8n | Google Drive | Google credential with root-folder access | Folder ID and file ID | Asset remains in Document Pending |
| n8n | Slack | Slack app OAuth credential | Channel ID and message timestamp | Task persists even if notification fails |
| Approval form | n8n and Tasks | Authenticated response through a staging sheet | Approval response ID | Unauthorized decisions are rejected and logged |
Microsoft Graph field mapping
| Graph source | Sheet field | Transformation |
|---|---|---|
user.id |
User_ID | Store unchanged as the stable identity key. |
user.userPrincipalName |
User_Email | Trim and convert to lowercase for matching. |
user.accountEnabled |
Account_Enabled | Convert to explicit TRUE or FALSE. |
user.department |
Department | Store blank when unavailable. |
user.signInActivity.lastSuccessfulSignInDateTime |
Last_Successful_Sign_In | Convert valid timestamps to UTC ISO 8601. |
user.assignedLicenses[].skuId |
SKU_ID | Normalize GUID text to lowercase. |
subscribedSku.skuPartNumber |
SKU_Part_Number | Map by SKU ID. |
subscribedSku.consumedUnits |
Consumed_Units | Store in the run summary and catalog comparison. |
For each destination write, n8n stored the external or returned identifier in the source record. For example, a successful Slack post returned a message timestamp stored in Slack_Message_TS. A successful Drive folder creation returned Document_Folder_ID.
The Graph workflow followed @odata.nextLink until no further page remained. The workflow did not assume that one response contained every user. If Graph returned HTTP 429, the workflow honored the Retry-After response header before retrying.
Step 5: Build the Core Automation
Automation A: Register a new physical asset
- Trigger: New unprocessed row in
Form_Asset_Receipt. - Conditions: Required fields exist; serial or tag is present; source event ID is not already processed.
- Actions: Normalize values, search for duplicates, generate Asset ID, create Drive folder, append transaction, create asset row, and mark the form row complete.
- Fields updated: Asset_ID, Status, Location, Condition, purchase fields, folder fields, transaction fields, timestamps, and automation status.
- Notification: Post to the private IT asset channel if inspection, tagging, or missing documentation remains.
- Exception: Duplicate serial, duplicate asset tag, inaccessible file, or invalid purchase date creates a manual-review task.
The exact processing order was:
- Create a run log row with status
Running. - Build
Source_Event_IDfrom form and response row. - Search the transaction sheet for that source event.
- If found, return the existing transaction and mark the form row
Duplicate Event Ignored. - Normalize serial number, asset tag, email, dates, and currency.
- Search active assets for matching serial number and asset tag.
- If a conflict exists, write an Errors row and stop before changing the asset master.
- Generate the Asset ID and Transaction ID.
- Create the asset’s Drive folder.
- Append the receipt transaction.
- Append the asset master row with status
Received. - Write returned folder and row identifiers.
- Post the inspection task to Slack if action remains.
- Mark the staging row and run log complete.
Automation B: Process check-in, check-out, transfer, and maintenance events
- Trigger: New unprocessed row in the movement or maintenance staging sheet.
- Conditions: Asset ID exists; event transition is permitted; authenticated requester has an allowed role.
- Actions: Read current state, validate transition, append transaction, update asset state, create or close related task, and post a notification when necessary.
- Fields updated: Custodian, location, status, condition, due dates, last transaction, timestamps, and automation status.
- Notification: Notify the new custodian, IT queue, or maintenance owner.
- Exception: Conflicting current custodian, invalid state transition, or missing due date sends the event to manual review.
An event could not move an asset directly from Disposed to Assigned. A correction required an authorized Correction transaction, reason, and approval by the IT manager.
Automation C: Reconcile Entra users and license assignments
- Trigger: Weekly schedule, with an authorized manual rerun option.
- Conditions: The previous run is not still active; Graph authentication succeeds; user and SKU pagination completes.
- Actions: Retrieve users and SKUs, create a run-specific assignment snapshot, compare exemptions, identify candidates, look up idempotency keys, create or update tasks, and notify owners.
- Fields updated: User_License_Snapshot, Tasks, Runs, Last_Automation_Run, retry fields, and Slack identifiers.
- Notification: Send one summary to IT plus individual task messages for newly created high-priority candidates.
- Exception: A partial Graph export does not replace the prior successful state and cannot automatically close tasks.
The initial candidate rules were conservative:
- Disabled account with an assigned reviewed SKU: high-priority removal review.
- Last successful sign-in at least 45 days ago: normal-priority usage review.
- No available sign-in activity: manual evidence review, not an unused-license conclusion.
- SKU absent from the license catalog: configuration task.
- Active exemption with an unexpired date: suppress inactivity review, but retain snapshot evidence.
Before appending a new task, n8n searched Tasks.Idempotency_Key. If a matching open task existed, the workflow updated Last_Automation_Run and Last Seen information without sending another initial alert.
Automation D: Create renewal and notice tasks
- Trigger: Daily schedule.
- Conditions: License status is Active; renewal or cancellation-notice date is within the configured lead period; no task exists for the SKU and date.
- Actions: Calculate projected commitment, assign owner, create task, and schedule approval steps.
- Fields updated: Task_ID, task type, owner, due date, approval status, and idempotency key.
- Notification: Post to the Finance software-renewal channel and notify the department owner.
- Exception: Missing cost, owner, renewal date, or contract evidence creates a data-completion task.
Automation E: Verify completed license removals
- Trigger: Successful weekly license snapshot.
- Conditions: Task status is Approved for Removal or Awaiting Verification.
- Actions: Compare the task’s User ID and SKU ID against the complete current assignment set.
- Fields updated: Status becomes Verified Removed and then Closed when the assignment is absent.
- Notification: Reply in the existing Slack thread or post a completion summary.
- Exception: If the assignment remains present, keep the task open and remind IT after the due date.
Step 6: Add Approvals, Reminders, and Escalations
Slack was used to request action, but it was not the authoritative approval system. Approvers submitted an authenticated Google Form response containing the task ID, decision, and comments. n8n checked the captured email against the expected owner or delegate before updating the task.
License removal approval
- The department owner received a review task containing user, SKU, reason, last available sign-in date, and due date.
- The owner selected
Keep,Remove,Reassign, orNeed Information. Keeprequired a reason and, for temporary conditions such as leave, an exemption expiry date.Removemoved the task toApproved for Removal.- IT removed the license manually in Entra ID and recorded the action time.
- The next successful Graph run verified whether the assignment was absent.
License renewal approval
The representative policy required a department owner decision for every paid renewal. A projected annual commitment of at least $5,000 required sequential approval from the department owner and controller. Lower commitments required the procurement owner and controller according to the company’s normal purchasing authority.
The threshold was stored in Lookups, not hard-coded into multiple workflows. Readers should replace it with their organization’s approved purchasing policy.
Equipment disposal approval
- IT submitted a retirement request with reason, condition, and data-bearing-device indicator.
- The IT manager confirmed backup requirements, account removal, encryption-key handling, and planned wipe or destruction evidence.
- After IT approval, Finance reviewed cost, age, residual value, and write-off information.
- Only after both approvals could the asset move to
Approvedfor disposal. - Final status became
Disposedonly after disposal date, method, vendor or internal method, and evidence link were recorded.
| Task type | First reminder | Repeat | Escalation |
|---|---|---|---|
| License review | Two days before due date | On due date | Department manager and IT manager after three overdue days |
| Renewal | Thirty days before notice date | Fourteen and seven days before notice date | Controller at seven days; executive owner if policy requires |
| Temporary equipment return | Three days before return | On due date and every two days overdue | Custodian’s manager after three overdue days |
| Maintenance | On target date | Every three days | IT manager after five overdue days |
| Disposal approval | Two days after assignment | Every three days | Backup approver after five days |
The Lookups sheet stored each approver’s backup. If an approver was marked unavailable, n8n reassigned the task to the authorized delegate and recorded the original owner, delegate, reason, and time. Delegation did not bypass approval thresholds.
Step 7: Add Documents and File Management
The Google Drive root used a predictable folder structure:
Asset Management/
Assets/
2026/
AST-2026-00184/
Purchase/
Warranty/
Maintenance/
Custody/
Disposal/
License Contracts/
SKU_GUID_OR_INTERNAL_CODE/
Current/
Previous/
Automation Evidence/
Runs/
Errors/
n8n created the asset folder after the duplicate check and before the asset master write. The returned folder ID was stored even if the human-readable folder name later changed.
Document names used an asset or task ID, document type, date, and short description, for example:
AST-2026-00184_Invoice_2026-07-03.pdf
AST-2026-00184_Warranty_2026-07-03.pdf
AST-2026-00184_Maintenance_2026-09-18.pdf
AST-2026-00184_DisposalCertificate_2029-11-22.pdf
TSK-2026-418_RenewalApproval_2026-10-01.pdf
Access inherited from controlled parent folders. Purchase cost and disposal documents were not shared through unrestricted public links. Slack messages linked to the asset or task record, not directly to sensitive files unless channel membership and Drive permissions were aligned.
Files were not silently replaced. A corrected document received a new version or a new dated filename. The task or transaction identified which file was current. Retention followed the company’s finance, security, employment, and legal policies rather than an arbitrary automation setting.
If folder creation or file transfer failed, the asset could still be registered when operationally necessary, but its automation status became Partial Failure. A document-recovery task retained the source upload reference and expected destination folder.
Step 8: Add Reporting and Operational Views
The reporting layer used protected Google Sheets tabs and filtered views. The Dashboard did not become a separate data source. It read from the asset, task, transaction, snapshot, and run sheets.
| View | Filter or calculation | Owner |
|---|---|---|
| New Assets | Status is Received or Automation_Status is Pending | IT support |
| Awaiting Action | Task status is New, Awaiting Owner, Awaiting IT, or Awaiting Finance | IT manager |
| Overdue Tasks | Due date before today and status not closed | Task owner |
| Incomplete Records | Required conditional field missing | IT support or Finance |
| Exceptions | Automation_Status is Manual Review, Partial Failure, or Automation Error | Automation owner |
| Rejected Items | Task status is Rejected | Process owner |
| Assets by Custodian | Group active assets by Custodian_Email | IT |
| Upcoming Warranty Expiry | Warranty date within 60 days | IT and procurement |
| Upcoming Renewals | Notice or renewal date within configured lead period | Finance |
| Unused-License Candidates | Latest successful snapshot where Candidate is TRUE | IT and department owners |
| Automation Failures | Open Errors rows or failed Runs | Automation owner |
| Processing Time | Completed timestamp minus created timestamp | IT manager |
| Manual-Review Queue | Status is Manual Review or Needs Information | Assigned owner |
Representative dashboard formulas included:
Open overdue tasks:
=COUNTIFS(Tasks!$L:$L,"<>Closed",Tasks!$M:$M,"<"&TODAY())
Assets in maintenance:
=COUNTIF(Assets!$H:$H,"Maintenance")
Warranty expiry in the next 60 days:
=COUNTIFS(Assets!$P:$P,">="&TODAY(),Assets!$P:$P,"<="&TODAY()+60)
Open automation errors:
=COUNTIF(Errors!$I:$I,"Open")
The formulas were placed on the Dashboard sheet. The workbook owner protected them and documented their source columns. Dashboard alert thresholds were stored in Lookups so that changes could be reviewed rather than hidden inside formulas.
Google Sheets recalculated operational views when source data changed. The Entra snapshot was only considered current when the corresponding Runs row had status Successful. A visible dashboard cell displayed the latest successful Graph run time.
Step 9: Add Security and Governance Controls
- Least privilege: The Entra integration was read-only. The Google automation identity received access only to the workbook and asset folders. The Slack app could post only where required.
- Role-based access: General employees submitted forms but did not edit the asset master. Finance cost columns and disposal evidence were restricted.
- Protected system fields: IDs, timestamps, workflow statuses, error counters, and integration identifiers were editable only by automation administrators.
- Credential storage: Secrets remained in n8n’s encrypted credential store or an approved secret manager. No credential appeared in code, Sheets, Slack, or execution notes.
- Shared-link controls: Drive files were restricted to named users or controlled groups. Public and unrestricted organization-wide links were disabled for sensitive folders.
- Activity evidence: Forms captured authenticated respondents. Transactions, tasks, Runs, approval responses, Graph snapshot dates, and Slack message timestamps created an audit trail.
- Former-user removal: Offboarding included removal from Google groups, Drive permissions, Slack channels, n8n roles, and Entra administration.
- Backups: The workbook and critical document folders were included in the organization’s approved backup or export process. Recovery was tested, not merely configured.
- Privacy: Reports showed only operationally necessary employee information. Cost and disposal details were not exposed in general Slack channels.
- AI restrictions: Optional AI processing excluded purchase cost, employee performance information, credentials, security secrets, and unredacted documents unless separately approved.
- Human approval: License removal, purchase decisions, renewal commitments, and disposal remained human-controlled.
Relevant accounting, privacy, employment, security, and electronic-waste requirements vary by jurisdiction and industry. The implementation owner was responsible for confirming applicable retention, disposal, tax, and audit rules with qualified internal or external advisers.
Step 10: Deploy and Test
- Build all workflows against the test workbook and test Slack channel.
- Use synthetic users, licenses, assets, serial numbers, invoices, and approval responses.
- Use read-only Entra production access only after the Graph workflow passes pagination and permission tests. If a nonproduction tenant is available, use it first.
- Load a controlled sample of 20 assets and two license SKUs.
- Run user acceptance testing with the IT support coordinator, IT manager, procurement analyst, controller, and two department managers.
- Correct mappings and document every accepted exception.
- Import the physical asset baseline in batches. Reconcile each batch against purchase and custody evidence.
- Activate physical-asset intake first.
- Activate reminders and disposal approvals second.
- Run Entra reconciliation in report-only mode for two cycles without sending manager notifications.
- Review false positives and create valid exemptions.
- Enable license task creation after the report-only review.
- Monitor daily during the first two weeks and weekly thereafter.
The rollback plan disabled workflow activation, preserved staging responses, and returned staff to controlled manual updates while the last successful asset master and run logs remained available. Workflows were never rolled back by deleting transaction history.
Launch documentation included form instructions, status definitions, approval responsibilities, error recovery, credential ownership, field mappings, and a support contact. A short Slack announcement told users which forms to use and clarified that direct messages would no longer count as completed asset updates.
Code and Configuration
The implementation used native n8n nodes for scheduling, HTTP requests, Google Sheets operations, Google Drive actions, Slack posting, conditions, routing, waits, and retries. A JavaScript Code node was required for license-assignment normalization and candidate evaluation.
Microsoft Graph authentication and requests
Configure an n8n OAuth2 client-credentials connection with the following placeholders. The credential, not the workflow body, stores the client secret.
Tenant ID: YOUR_TENANT_ID
Client ID: YOUR_CLIENT_ID
Client secret: YOUR_CLIENT_SECRET
Token URL: https://login.microsoftonline.com/YOUR_TENANT_ID/oauth2/v2.0/token
Scope: https://graph.microsoft.com/.default
Grant type: client_credentials
The credential requests a token using the permissions granted to the Entra application. The expected token response contains an access token and expiry information. n8n refreshes by requesting another application token when required.
Configure one paginated HTTP Request node for users:
Method: GET
URL: https://graph.microsoft.com/v1.0/users
Query parameter $select:
id,displayName,userPrincipalName,userType,accountEnabled,department,officeLocation,assignedLicenses,signInActivity
Query parameter $top:
999
Authentication:
Microsoft Graph client-credentials connection
Pagination:
Use the URL returned in @odata.nextLink until the property is absent
Configure a second request for subscribed SKUs:
Method: GET
URL: https://graph.microsoft.com/v1.0/subscribedSkus
Authentication:
Microsoft Graph client-credentials connection
Expected fields:
skuId,skuPartNumber,consumedUnits,prepaidUnits,capabilityStatus
n8n interface labels vary between versions. The required behavior is to issue each GET request with a valid bearer token, preserve all pages, stop only when @odata.nextLink is absent, and fail the run if any page cannot be retrieved.
A representative user response has this shape:
{
"value": [
{
"id": "USER_OBJECT_ID",
"displayName": "Sample User",
"userPrincipalName": "[email protected]",
"userType": "Member",
"accountEnabled": true,
"department": "Engineering",
"officeLocation": "Remote",
"assignedLicenses": [
{
"skuId": "SKU_GUID",
"disabledPlans": []
}
],
"signInActivity": {
"lastSuccessfulSignInDateTime": "2026-06-01T14:22:10Z"
}
}
],
"@odata.nextLink": "NEXT_PAGE_URL_IF_PRESENT"
}
Do not log access tokens or complete response payloads in Slack. The Runs sheet should record counts, timestamps, status, and sanitized error details.
Prepare the Code node input
Aggregate all user pages into a usersskus array. Read the Licenses and Exemptions sheets and assemble one input item with this structure:
{
"runId": "LIC-20260715T020000Z",
"runAt": "2026-07-15T02:00:00Z",
"inactiveDays": 45,
"users": [],
"skus": [],
"licenseCatalog": [],
"exemptions": []
}
In an n8n Edit Fields or Set node, arrays can be referenced from named upstream aggregate nodes. Confirm the actual output shape in the execution preview before activating the workflow.
Complete n8n Code node for license comparison
Place the following code in an n8n Code node configured to run once for all items. It validates the assembled payload, normalizes identifiers, maps assignments, checks exemptions, calculates inactivity, and returns one item per observed assignment.
const inputItem = $input.first();
if (!inputItem || !inputItem.json) {
throw new Error('The license comparison node received no input.');
}
const payload = inputItem.json;
if (!Array.isArray(payload.users)) {
throw new Error('payload.users must be an array.');
}
if (!Array.isArray(payload.skus)) {
throw new Error('payload.skus must be an array.');
}
if (!Array.isArray(payload.licenseCatalog)) {
throw new Error('payload.licenseCatalog must be an array.');
}
if (!Array.isArray(payload.exemptions)) {
throw new Error('payload.exemptions must be an array.');
}
function normalizeText(value) {
return value === null || value === undefined
? ''
: String(value).trim();
}
function normalizeId(value) {
return normalizeText(value).toLowerCase();
}
function normalizeEmail(value) {
return normalizeText(value).toLowerCase();
}
function parseBoolean(value, defaultValue = false) {
if (typeof value === 'boolean') {
return value;
}
const normalized = normalizeText(value).toLowerCase();
if (['true', 'yes', '1', 'enabled'].includes(normalized)) {
return true;
}
if (['false', 'no', '0', 'disabled'].includes(normalized)) {
return false;
}
return defaultValue;
}
function parseDate(value) {
if (!value) {
return null;
}
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) {
return null;
}
return parsed;
}
function toIsoOrBlank(dateValue) {
return dateValue ? dateValue.toISOString() : '';
}
function addDays(dateValue, days) {
const result = new Date(dateValue.getTime());
result.setUTCDate(result.getUTCDate() + days);
return result;
}
const runAt = parseDate(payload.runAt) || new Date();
const inactiveDays = Number(payload.inactiveDays ?? 45);
if (!Number.isFinite(inactiveDays) || inactiveDays < 1) {
throw new Error('inactiveDays must be a positive number.');
}
const runId =
normalizeText(payload.runId) ||
`LIC-${runAt.toISOString().replace(/[-:.]/g, '')}`;
const skuMap = new Map();
for (const sku of payload.skus) {
const skuId = normalizeId(sku.skuId);
if (!skuId) {
continue;
}
skuMap.set(skuId, {
skuId,
skuPartNumber: normalizeText(sku.skuPartNumber),
consumedUnits:
Number.isFinite(Number(sku.consumedUnits))
? Number(sku.consumedUnits)
: null,
capabilityStatus: normalizeText(sku.capabilityStatus)
});
}
const catalogMap = new Map();
for (const row of payload.licenseCatalog) {
const skuId = normalizeId(row.SKU_ID);
if (!skuId) {
continue;
}
catalogMap.set(skuId, {
displayName:
normalizeText(row.Display_Name) ||
normalizeText(row.SKU_Part_Number) ||
skuId,
ownerEmail: normalizeEmail(row.Owner_Email),
reviewEnabled: parseBoolean(row.Review_Enabled, true),
status: normalizeText(row.Status) || 'Active'
});
}
const exemptionMap = new Map();
for (const row of payload.exemptions) {
const userId = normalizeId(row.User_ID);
const skuId = normalizeId(row.SKU_ID);
if (!userId || !skuId) {
continue;
}
const key = `${userId}|${skuId}`;
exemptionMap.set(key, {
exemptUntil: parseDate(row.Exempt_Until),
reason: normalizeText(row.Reason),
approvedBy: normalizeEmail(row.Approved_By)
});
}
const output = [];
let assignmentCount = 0;
for (const user of payload.users) {
const userId = normalizeId(user.id);
const userEmail = normalizeEmail(user.userPrincipalName);
const assignedLicenses = Array.isArray(user.assignedLicenses)
? user.assignedLicenses
: [];
if (!userId) {
continue;
}
const accountEnabled = user.accountEnabled === true;
const signInActivity = user.signInActivity || {};
const lastSignIn =
parseDate(signInActivity.lastSuccessfulSignInDateTime) ||
parseDate(signInActivity.lastSignInDateTime);
let daysInactive = null;
if (lastSignIn) {
const differenceMs = runAt.getTime() - lastSignIn.getTime();
daysInactive = Math.max(
0,
Math.floor(differenceMs / (1000 * 60 * 60 * 24))
);
}
for (const assignedLicense of assignedLicenses) {
const skuId = normalizeId(assignedLicense.skuId);
if (!skuId) {
continue;
}
assignmentCount += 1;
const graphSku = skuMap.get(skuId) || {
skuId,
skuPartNumber: '',
consumedUnits: null,
capabilityStatus: ''
};
const catalog = catalogMap.get(skuId);
const exemption = exemptionMap.get(`${userId}|${skuId}`);
const exemptionActive =
Boolean(exemption?.exemptUntil) &&
exemption.exemptUntil.getTime() >= runAt.getTime();
let candidateReason = '';
if (!catalog) {
candidateReason = 'UNMAPPED_SKU';
} else if (catalog.status.toLowerCase() !== 'active') {
candidateReason = 'INACTIVE_CATALOG_SKU';
} else if (!accountEnabled) {
candidateReason = 'DISABLED_ACCOUNT';
} else if (!lastSignIn) {
candidateReason = 'NO_SIGNIN_DATA';
} else if (daysInactive >= inactiveDays) {
candidateReason = `INACTIVE_${inactiveDays}_DAYS`;
}
const reviewEnabled = catalog ? catalog.reviewEnabled : true;
const exemptionSuppresses =
exemptionActive && candidateReason !== 'DISABLED_ACCOUNT';
const candidate =
Boolean(candidateReason) &&
reviewEnabled &&
!exemptionSuppresses;
let priority = 'Normal';
if (
candidateReason === 'DISABLED_ACCOUNT' ||
candidateReason === 'UNMAPPED_SKU'
) {
priority = 'High';
}
const displayName =
catalog?.displayName ||
graphSku.skuPartNumber ||
skuId;
const taskKey = candidate
? `LICREV:${userId}:${skuId}:${candidateReason}`
: '';
const dueDate = addDays(runAt, priority === 'High' ? 3 : 7);
output.push({
json: {
Record_Type: 'Assignment',
Run_ID: runId,
Snapshot_Key: `${runId}:${userId}:${skuId}`,
Snapshot_Date: runAt.toISOString(),
User_ID: userId,
User_Name: normalizeText(user.displayName),
User_Email: userEmail,
User_Type: normalizeText(user.userType),
Department: normalizeText(user.department),
Office_Location: normalizeText(user.officeLocation),
Account_Enabled: accountEnabled,
Last_Successful_Sign_In: toIsoOrBlank(lastSignIn),
Days_Inactive: daysInactive,
SKU_ID: skuId,
SKU_Part_Number: graphSku.skuPartNumber,
License_Display_Name: displayName,
Candidate: candidate,
Candidate_Reason: candidateReason,
Exempt_Until: toIsoOrBlank(exemption?.exemptUntil || null),
Exemption_Reason: exemption?.reason || '',
Exemption_Approved_By: exemption?.approvedBy || '',
Task_Idempotency_Key: taskKey,
Suggested_Priority: priority,
Suggested_Due_Date: dueDate.toISOString(),
Suggested_Owner_Email:
catalog?.ownerEmail || 'YOUR_EMAIL_ADDRESS',
Task_Title: candidate
? `Review ${displayName} for ${userEmail || userId}`
: '',
Automation_Status: 'Compared',
Error_Message: ''
}
});
}
}
if (output.length === 0) {
return [
{
json: {
Record_Type: 'Control',
Run_ID: runId,
Snapshot_Date: runAt.toISOString(),
Assignment_Count: assignmentCount,
Automation_Status: 'No assignments returned',
Error_Message: ''
}
}
];
}
return output;
The code has no external dependencies. It runs inside the n8n Code node after Graph pagination and Google Sheets reads have completed. Replace YOUR_EMAIL_ADDRESS with the fallback license owner or supply an owner for every active catalog row.
Test the node with at least one enabled active user, one disabled user, one user without activity data, one exempt user, and one unknown SKU. Inspect the n8n execution output before connecting it to write nodes.
Likely errors include arrays arriving as text, renamed Google Sheet headers, invalid date formats, and incomplete Graph pagination. Correct the upstream mapping rather than weakening input validation.
Route and write the comparison output
- Use a filter to retain records where
Record_TypeequalsAssignment. - Append all assignment records to
User_License_Snapshot. - Create a second branch where
Candidateequals TRUE. - For each candidate, search Tasks by
Task_Idempotency_Key. - If no row exists, generate a Task ID, append the task, post to Slack, and write the returned Slack timestamp.
- If an open row exists, update its last-seen run and due-state fields without posting a duplicate initial message.
- If a closed row exists and the same condition recurs, route it to manual review before reopening it.
- After all writes succeed, set the Runs row to
Successful.
Set the license workflow to one active execution at a time. This prevents two overlapping scheduled or manual runs from creating competing tasks.
Slack configuration
Use the native Slack node or an authenticated HTTP request to chat.postMessage. The message should contain a minimal operational summary:
{
"channel": "YOUR_SLACK_CHANNEL_ID",
"text": "License review task TSK-2026-418 requires action. Review the assigned license, supporting activity date, and due date in the controlled task register.",
"unfurl_links": false,
"unfurl_media": false
}
With the native Slack node, map the task’s channel and message text into the node fields. Store the successful response’s channel identifier and ts value in the Tasks sheet. If Slack fails, do not roll back the task. Set Automation_Status to Partial Failure and retry notification separately.
Retry and deployment configuration
Configure recoverable HTTP requests for a maximum of three automated attempts. Use increasing waits such as 30 seconds, 120 seconds, and 300 seconds for transient network or server errors. For Graph HTTP 429, honor Retry-After when supplied.
Authentication errors such as HTTP 401 or 403 should not be retried repeatedly. Record the failure, stop dependent writes, notify the automation administrator, and verify credentials, consent, scope, and application status.
Activate workflows only after replacing workbook IDs, Drive folder IDs, channel IDs, tenant credentials, fallback owner addresses, and threshold values. Review execution logs in n8n and compare Runs counts with the number of snapshot rows written.
Failure Handling and Operational Reliability
| Failure | What users see | Automated response | Manual recovery | Owner |
|---|---|---|---|---|
| Missing required field | Submission remains Needs Information. | No master update; task created. | Submit corrected information referencing the source event. | Requester or IT |
| Duplicate form event | Existing transaction ID is returned. | Duplicate event ignored through Source_Event_ID. | No action unless content differs. | Automation owner |
| Duplicate asset record | Manual Review status. | Master write stops before creation. | Compare serial, tag, and purchase evidence. | IT manager |
| Invalid status transition | Event appears in exception queue. | No current-state update. | Correct event or submit approved correction. | IT manager |
| Partial Google Sheets write | Partial Failure status. | Record identifiers and completed steps are retained. | Replay only missing idempotent steps. | Automation owner |
| Graph authentication expiry or revocation | License dashboard becomes stale. | Run fails and no tasks are closed. | Restore credential or consent, then rerun. | Entra administrator |
| Graph throttling | No immediate user-facing effect. | Wait according to Retry-After and retry. | Reduce frequency or request volume if persistent. | Automation owner |
| Incomplete Graph pagination | Run marked Failed. | Prior successful snapshot remains authoritative. | Correct pagination and rerun. | Automation owner |
| Sign-in activity missing | Manual evidence review task. | Reason set to NO_SIGNIN_DATA. | Review account type, manager confirmation, and other approved evidence. | IT and department owner |
| Unavailable approver | Task remains open with delegation note. | Assign approved backup after configured delay. | Process owner confirms delegation. | IT or Finance owner |
| Unauthorized approval response | Decision is not applied. | Response logged as rejected. | Authorized approver resubmits. | Process owner |
| Failed folder creation | Asset shows Document Pending. | Asset registration continues only when allowed. | Correct permission and replay folder step. | Google administrator |
| Failed file upload | Document task remains open. | Source reference retained; retry attempted. | Upload directly to controlled folder and link evidence. | Requester or IT |
| Invalid email address | Owner assignment fails validation. | Route to fallback queue. | Correct lookup or user data. | IT support |
| Slack notification failure | No message, but task exists. | Retry notification and mark Partial Failure. | Work from task queue and repair Slack connection. | Automation owner |
| Workflow timeout | Run remains Failed or Timed Out. | No completion flag is written. | Inspect last successful step and replay safely. | Automation owner |
| Repeated unrecovered failure | Error appears on dashboard. | Write an open Errors record after retry limit. | Correct cause and use documented replay workflow. | Named error owner |
The Errors sheet served as a dead-letter register. It contained references to failed events rather than unrestricted copies of sensitive payloads. Each row had an owner, recovery status, retry count, and resolution note.
Idempotency was applied at three levels:
- Source events used a stable form and response-row key.
- License and renewal tasks used deterministic business keys.
- External identifiers such as Drive folder IDs and Slack message timestamps were stored after successful creation.
A daily reconciliation workflow identified staging responses marked Processing for more than 30 minutes, assets whose last transaction did not exist, open tasks with invalid owners, missing Drive folder IDs, and failed runs without an Errors entry.
A Complete Example
An employee, Morgan Lee, returned a laptop during an internal role change. The example email address was [email protected], and the laptop was already registered as AST-2026-00184.
- IT submitted the Asset Movement form with event type
Check In, asset IDAST-2026-00184, observed conditionFair, destinationIT Storage, and a note that the battery was draining unusually quickly. - Google Forms wrote response row 1842. n8n created source event ID
MOVE:1842and confirmed that it had not been processed. - n8n read the asset master. The previous status was Assigned, and the custodian matched Morgan’s email, so the transition was valid.
- The workflow generated transaction ID
TXN-20260715-01842and appended the event toAsset_Transactions. - The asset master changed to status
Maintenance, locationIT Storage, blank custodian, conditionFair, and last transactionTXN-20260715-01842. - A maintenance task was created for IT support with a five-business-day target. A Slack message referenced the task and asset ID.
- During the weekly Entra reconciliation, Morgan’s user object was still enabled but showed 53 days since the last available successful sign-in. The account retained an assigned paid planning license with SKU ID
11111111-2222-3333-4444-555555555555. - The Code node evaluated the 45-day inactivity threshold. No exemption existed, so it generated idempotency key
LICREV:USER_OBJECT_ID:11111111-2222-3333-4444-555555555555:INACTIVE_45_DAYS. - n8n found no existing task with that key and created a normal-priority license review assigned to the department manager.
- The manager confirmed that Morgan no longer needed the planning license. The task moved to
Approved for Removal. - IT removed the license manually in Entra ID and recorded the action time. The automation did not have write permission to perform removal.
- The next successful Graph run did not find the User ID and SKU ID assignment pair. n8n moved the task to
Verified Removed, recorded the verification run ID, and closed it. - The laptop repair was completed separately. IT recorded the replacement battery, repair evidence link, condition
Good, and statusAvailable.
If the Graph run had failed midway through pagination, the license task would not have closed. The prior snapshot and open task would have remained unchanged until a complete successful run verified removal.
Implementation Cost
All amounts below are representative planning assumptions, not quoted vendor prices or verified client costs. Existing Entra ID, Google Workspace, and Slack subscriptions were treated as existing business costs. Organizations must confirm whether their current subscriptions include the required permissions, activity data, API access, storage, and administrative features.
| Activity | Hours | Assumed rate | Estimated cost |
|---|---|---|---|
| Requirements and process design | 12 | $70 per hour | $840 |
| Data cleanup and baseline import | 16 | $70 per hour | $1,120 |
| Workbook, forms, and Drive structure | 18 | $70 per hour | $1,260 |
| n8n, Graph, Sheets, Drive, and Slack workflows | 28 | $70 per hour | $1,960 |
| Testing and user acceptance | 16 | $70 per hour | $1,120 |
| Training | 6 | $70 per hour | $420 |
| Documentation and handover | 8 | $70 per hour | $560 |
| Total internal implementation assumption | 104 | $7,280 |
| Item | Type | Monthly assumption |
|---|---|---|
| n8n hosting or managed execution allowance | Automation | $75 |
| Backup, monitoring, and secret-management allowance | Operational tooling | $20 |
| Existing Entra ID, Google Workspace, and Slack subscriptions | Existing software | $0 incremental assumption |
| Monthly maintenance labor | Internal labor, four hours at $70 | $280 |
| Optional AI usage allowance | Optional API usage | $12 to $20 |
An optional professional implementation could reasonably be budgeted separately based on scope, data quality, security review, deployment model, and support requirements. For planning purposes, the representative range was $14,000 to $24,000, excluding internal subject-matter participation and vendor subscription costs. This was an alternative sourcing assumption, not an amount added to the internal-labor total.
Estimated Time and Cost Savings
The savings model used the following representative assumptions:
- 100 actionable workflow records per month
- 28 asset custody or maintenance events
- 12 employee lifecycle events
- 60 license, renewal, exception, or follow-up reviews
- 560 license assignments scanned automatically each week
- 18 minutes of current handling time per actionable record
- 5 minutes of new handling time per actionable record
- 12 percent exception rate
- 10 minutes of manual review for each exception
- 4 hours of monthly workflow maintenance
- $52 loaded hourly labor cost
- $95 monthly recurring tool allowance
- $7,280 one-time implementation cost
Current monthly labour hours: Monthly volume × current minutes per record ÷ 60
New monthly labour hours: Monthly volume × new minutes per record ÷ 60, plus exception handling and maintenance
Monthly hours recovered: Current monthly labour hours minus new monthly labour hours
Estimated monthly labour value: Monthly hours recovered × loaded hourly labour cost
Net estimated monthly value: Monthly labour value minus recurring tool costs
Estimated payback period: One-time implementation cost ÷ net estimated monthly value
| Calculation | Formula | Result |
|---|---|---|
| Current monthly labor | 100 × 18 ÷ 60 | 30.00 hours |
| New routine handling | 100 × 5 ÷ 60 | 8.33 hours |
| Exception handling | 12 × 10 ÷ 60 | 2.00 hours |
| Maintenance | 4 hours | 4.00 hours |
| Total new monthly labor | 8.33 + 2.00 + 4.00 | 14.33 hours |
| Monthly hours recovered | 30.00 – 14.33 | 15.67 hours |
| Monthly labor value | 15.67 × $52 | Approximately $815 |
| Net monthly value | $815 – $95 | Approximately $720 |
| Estimated payback | $7,280 ÷ $720 | Approximately 10.1 months |
Recovered time does not automatically reduce payroll. It may create capacity for faster onboarding and offboarding, more timely maintenance, reduced overtime, improved support, higher transaction volume, and less dependency on individual employees.
Non-financial benefits included clearer custody, fewer follow-up messages, more consistent records, faster renewal preparation, better disposal evidence, improved auditability, visible automation failures, and more dependable reporting for IT and Finance.
Readers should replace the transaction volume, handling time, exception rate, labor rate, maintenance effort, software allowance, and implementation cost with their own measured figures. A short time study conducted before implementation provides a more reliable baseline than estimates based on memory.
Adding AI to the Automation
AI was treated as an optional enhancement after forms, validation, identity integration, task routing, approvals, and failure recovery worked reliably.
Several possible AI applications were considered:
- Summarizing long maintenance and check-in notes
- Suggesting a maintenance issue category
- Identifying likely missing diagnostic information
- Extracting asset and warranty information from supplier documents
- Comparing purchase documents with registered asset fields
- Searching historical maintenance notes by meaning rather than exact keywords
- Identifying unusual combinations of repair cost, age, and repeated failures for human review
Required fields, exact serial-number matching, date calculations, renewal thresholds, user-ID matching, status transitions, permissions, and approval rules remained deterministic. AI was not needed for those tasks.
The core automation created the asset record, synchronized identity data, detected exact conditions, generated tasks, routed approvals, and sent reminders. AI added value only where staff had to interpret unstructured written notes.
The Recommended AI Enhancement
The recommended enhancement summarized maintenance and check-in notes, suggested a category, and identified missing diagnostic information. It did not approve replacement, disposal, repair expenditure, or employee action.
- Trigger: A validated Check In or Maintenance Open transaction with at least 20 characters of free-text notes.
- AI input: Asset category, model, condition, event type, and redacted operational notes.
- System instruction: Classify and summarize only the provided maintenance information without inventing facts.
- Expected output: Structured JSON containing category, summary, missing information, priority suggestion, confidence, and review requirement.
- Validation: n8n checks allowed categories, data types, confidence range, and required fields.
- Record update: Valid results are written to advisory AI fields on the transaction or task.
- Human review: IT confirms or corrects the category before action.
- Low confidence: Confidence below 0.75 forces manual review.
- Prohibited data: Credentials, authentication codes, employee performance information, personal health data, unrestricted invoices, and security secrets.
- Failure behavior: The normal maintenance task continues without AI output.
Reusable system instruction
You are assisting an internal IT asset-management team.
Use only the information supplied in the user message. Do not infer warranty coverage, root cause, repair cost, safety, employee responsibility, or disposal eligibility.
Summarize the reported issue, select one allowed category, identify missing diagnostic information, and suggest a review priority. Your priority is advisory and must not authorize repair, replacement, disposal, or employee action.
Return only JSON that conforms to the supplied schema. If the information is ambiguous, lower confidence and set review_required to true.
Reusable user prompt
Classify this asset event.
Asset category: {{Category}}
Model: {{Model}}
Event type: {{Event_Type}}
Reported condition: {{Condition}}
Operational notes: {{Redacted_Notes}}
Allowed categories:
hardware_damage
battery_or_power
display_or_video
network_or_connectivity
software_or_configuration
performance
peripheral
cosmetic
missing_accessory
no_fault_reported
other
Identify missing information only from:
error_message
reproduction_steps
power_status
physical_damage_details
accessory_list
recent_changes
test_results
photo_or_document
none
Structured output schema
{
"type": "object",
"additionalProperties": false,
"required": [
"category",
"summary",
"missing_information",
"priority_suggestion",
"confidence",
"review_required",
"rationale"
],
"properties": {
"category": {
"type": "string",
"enum": [
"hardware_damage",
"battery_or_power",
"display_or_video",
"network_or_connectivity",
"software_or_configuration",
"performance",
"peripheral",
"cosmetic",
"missing_accessory",
"no_fault_reported",
"other"
]
},
"summary": {
"type": "string",
"maxLength": 300
},
"missing_information": {
"type": "array",
"items": {
"type": "string",
"enum": [
"error_message",
"reproduction_steps",
"power_status",
"physical_damage_details",
"accessory_list",
"recent_changes",
"test_results",
"photo_or_document",
"none"
]
},
"maxItems": 5
},
"priority_suggestion": {
"type": "string",
"enum": [
"low",
"normal",
"high",
"urgent_review"
]
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"review_required": {
"type": "boolean"
},
"rationale": {
"type": "string",
"maxLength": 300
}
}
}
Configure the approved model node for deterministic output where possible, with low temperature and structured-output enforcement. Put the parsed response in a field named ai_result, then pass it through the following validation Code node.
const items = $input.all();
const allowedCategories = new Set([
'hardware_damage',
'battery_or_power',
'display_or_video',
'network_or_connectivity',
'software_or_configuration',
'performance',
'peripheral',
'cosmetic',
'missing_accessory',
'no_fault_reported',
'other'
]);
const allowedMissingInformation = new Set([
'error_message',
'reproduction_steps',
'power_status',
'physical_damage_details',
'accessory_list',
'recent_changes',
'test_results',
'photo_or_document',
'none'
]);
const allowedPriorities = new Set([
'low',
'normal',
'high',
'urgent_review'
]);
function parseAiResult(value) {
if (value && typeof value === 'object' && !Array.isArray(value)) {
return value;
}
if (typeof value === 'string') {
return JSON.parse(value);
}
throw new Error('ai_result must be a JSON object or JSON string.');
}
return items.map((item) => {
const output = { ...item.json };
try {
const result = parseAiResult(item.json.ai_result);
if (!allowedCategories.has(result.category)) {
throw new Error('Invalid AI category.');
}
if (
typeof result.summary !== 'string' ||
result.summary.trim().length === 0 ||
result.summary.length > 300
) {
throw new Error('Invalid AI summary.');
}
if (!Array.isArray(result.missing_information)) {
throw new Error('missing_information must be an array.');
}
if (result.missing_information.length > 5) {
throw new Error('Too many missing-information values.');
}
for (const value of result.missing_information) {
if (!allowedMissingInformation.has(value)) {
throw new Error(`Invalid missing-information value: ${value}`);
}
}
if (!allowedPriorities.has(result.priority_suggestion)) {
throw new Error('Invalid priority suggestion.');
}
if (
typeof result.confidence !== 'number' ||
result.confidence < 0 ||
result.confidence > 1
) {
throw new Error('AI confidence must be between 0 and 1.');
}
if (typeof result.review_required !== 'boolean') {
throw new Error('review_required must be a boolean.');
}
if (
typeof result.rationale !== 'string' ||
result.rationale.length > 300
) {
throw new Error('Invalid AI rationale.');
}
output.AI_Category = result.category;
output.AI_Summary = result.summary.trim();
output.AI_Missing_Information =
result.missing_information.join(', ');
output.AI_Priority_Suggestion = result.priority_suggestion;
output.AI_Confidence = result.confidence;
output.AI_Rationale = result.rationale.trim();
output.AI_Status = 'Valid';
output.AI_Error = '';
output.AI_Review_Required =
result.review_required ||
result.confidence < 0.75 ||
result.priority_suggestion === 'urgent_review';
} catch (error) {
output.AI_Category = '';
output.AI_Summary = '';
output.AI_Missing_Information = '';
output.AI_Priority_Suggestion = '';
output.AI_Confidence = '';
output.AI_Rationale = '';
output.AI_Status = 'Invalid';
output.AI_Error =
error instanceof Error ? error.message : String(error);
output.AI_Review_Required = true;
}
return { json: output };
});
This node requires no external dependencies. It runs after the AI model or structured-output parser. Test it with valid JSON, malformed JSON, unknown categories, confidence outside the allowed range, and missing required properties.
Store the model name, prompt version, timestamp, confidence, validation status, and reviewer correction. Do not store hidden reasoning or unrestricted prompt content. If the AI service is unavailable, set AI_Status to Unavailable and continue with the standard human workflow.
Benefits of the AI Enhancement
The AI-specific benefit was limited to unstructured maintenance information. It could reduce the time needed to read long notes, produce consistent categories, identify absent troubleshooting details, and make maintenance themes easier to report.
It did not create the asset register, identify exact duplicate serial numbers, compare license GUIDs, calculate warranty dates, create deterministic renewal tasks, route approvals, or verify license removal. Those benefits came from normal validation, integration, formulas, and workflow automation.
Human review also provided feedback on whether the categories were useful. If staff routinely corrected the same category or ignored the output, the prompt, taxonomy, or business case required review.
What Remains Rule-Based or Human-Controlled
| Decision | Control type | Reason |
|---|---|---|
| Serial-number duplicate detection | Rule-based | Exact normalized matching is more reliable and explainable. |
| Warranty and renewal deadlines | Rule-based | Date arithmetic does not require AI. |
| License assignment comparison | Rule-based | User and SKU identifiers support deterministic matching. |
| Whether a user still needs a license | Human-controlled | Sign-in activity does not reveal every business requirement. |
| License removal | Human-controlled | Removal can interrupt work and requires authorization. |
| Renewal commitment | Human-controlled | It creates a financial obligation. |
| Repair approval | Human-controlled | Cost, safety, operational impact, and warranty terms require review. |
| Disposal and accounting treatment | Human-controlled | Security, finance, legal, and environmental obligations apply. |
| Data-wipe sufficiency | Human-controlled | Security evidence must meet organizational policy. |
| Final maintenance priority | Human-controlled | AI has incomplete operational context and may be inaccurate. |
Estimating the Additional Value of AI
The representative estimate assumed 35 maintenance or check-in records each month were eligible for AI analysis. Without AI, staff spent about 2.5 minutes reading and categorizing the unstructured notes within the broader five-minute automated process.
| Assumption | Value |
|---|---|
| AI-eligible records | 35 per month |
| Gross time saved per eligible record | 1.5 minutes |
| Gross time saved | 52.5 minutes |
| Expected correction rate | 15 percent |
| Correction time | 1 minute per correction |
| Expected service or parsing failure rate | 3 percent |
| Fallback time | 2.5 minutes per failure |
| Net additional capacity | Approximately 45 minutes per month |
| Loaded labor value | Approximately $39 per month |
| AI usage allowance | $12 per month |
| Net estimated incremental value | Approximately $27 per month |
At this volume, AI would be justified primarily by consistency and improved maintenance reporting rather than substantial direct savings. The company could defer the enhancement until maintenance-note volume or complexity increased.
AI did not eliminate corrections, failures, or human review. Cost and quality needed monitoring by prompt version and record type.
Testing Checklist
Use synthetic sample data before processing real employee, asset, license, financial, or supplier information.
| Test | Expected result |
|---|---|
| Normal asset receipt | One asset, one transaction, one folder, and correct IDs are created. |
| Missing required field | No master update; Needs Information task created. |
| Invalid date or currency | Validation fails with a useful error. |
| Duplicate serial number | New asset creation stops and manual review begins. |
| Duplicate form submission | Source event is ignored without duplicate transaction. |
| Duplicate workflow event | Idempotency key returns the existing task or record. |
| Valid check-out | Custodian, due date, status, and history update correctly. |
| Invalid state transition | Asset master remains unchanged. |
| Successful Graph pagination | All pages contribute to the run count. |
| Failed Graph page | Run fails and no removal task is closed. |
| Failed authentication | Workflow stops, logs the failure, and alerts the administrator. |
| Expired or revoked credential | No dependent writes occur until access is restored. |
| Graph HTTP 429 | Workflow honors retry timing and continues if recovery succeeds. |
| Disabled user with license | High-priority review task is created once. |
| Inactive user with license | Normal review task is created according to threshold. |
| Missing sign-in activity | Manual-review reason is recorded without asserting nonuse. |
| Valid exemption | Inactivity task is suppressed until expiry. |
| Unavailable approver | Authorized delegation is recorded and used. |
| Unauthorized approval | Decision is rejected and logged. |
| Approval rejection | Task records reason and follows the rejection path. |
| Reassignment | Old and new owners remain visible in history. |
| Overdue task | Reminder and escalation occur at configured times. |
| Failed folder creation | Partial Failure status and recovery task are created. |
| Failed file upload | Source reference remains available for manual recovery. |
| Failed Slack notification | Task persists and notification retries separately. |
| Unauthorized workbook user | Protected data and system columns cannot be changed. |
| Malformed AI output | Validation fails and standard human review continues. |
| Inaccurate AI output | Reviewer can correct it without changing source notes. |
| AI service failure | Maintenance workflow continues without AI. |
| Successful license removal | Later complete Graph run verifies absence and closes the task. |
| Correct reporting | Dashboard totals reconcile to source sheets. |
| Correct audit record | Source event, task, approvals, run, and external IDs are linked. |
| Correct retry behavior | Recoverable failure retries without duplicate business records. |
Ongoing Maintenance
| Frequency | Activity | Owner |
|---|---|---|
| Daily | Review failed runs, open Errors rows, stale Processing events, and overdue high-priority tasks. | IT support |
| Weekly | Review license reconciliation, unmapped SKUs, duplicate candidates, notification failures, and open manual-review records. | IT manager |
| Monthly | Reconcile asset counts, review maintenance metrics, inspect workflow execution volume, and review automation cost. | IT and Finance |
| Quarterly | Review permissions, approver and delegate mappings, inactive users, shared links, thresholds, and dashboard accuracy. | System owner and administrators |
| Quarterly | Sample AI output, correction rates, invalid responses, prompt version, and data restrictions. | IT manager |
| Semiannually | Test backup restoration, credential ownership, replay procedure, and failure notifications. | Automation owner |
| Annually | Review retention, disposal rules, Graph permissions, Slack scopes, Google access, vendor changes, and platform suitability. | IT, Finance, security, and governance owners |
| On employee departure | Remove n8n roles, Google access, Slack channel access, approval rights, and administrative permissions. | IT administrator |
The primary owner maintained workflow documentation, field mappings, status definitions, credentials, and error-recovery instructions. The backup owner performed at least one supervised recovery or test each quarter so that continuity did not depend on one employee.
Changes to form questions, sheet headers, API permissions, Slack channels, approval thresholds, and Drive folder ownership were treated as controlled changes. They were tested in the test workbook before production deployment.
Snapshots older than the approved retention period were archived only after run summaries and required audit evidence were preserved. Archiving was documented and reversible.
When to Move to Dedicated Software
The implementation could continue while transaction volume, concurrency, permissions, and reporting needs remained manageable. Replacing it solely because it used spreadsheets would not be necessary.
Migration to dedicated IT asset management, software asset management, configuration management, or service management software should be evaluated when several of these conditions appear:
- Thousands of active assets or rapidly increasing transaction volume
- Multiple simultaneous updates that exceed spreadsheet concurrency controls
- Need for automatic device discovery, software metering, or endpoint telemetry
- Complex stockrooms, warehouses, legal entities, or international locations
- Advanced role-based permissions at record or field level
- Formal audit requirements beyond the evidence the workbook can provide
- Regulated retention, chain-of-custody, or validated-system requirements
- Increasing exception rates caused by spreadsheet limitations
- Excessive maintenance after normal platform changes
- Need for a customer-facing or employee self-service portal
- Barcode or QR scanning at scale
- Mobile or offline inventory requirements
- Complex procurement, depreciation, tax, or accounting integrations
- Vendor support and service-level commitments becoming mandatory
- Advanced forecasting, software-usage analytics, or contract optimization
- Security risk exceeding the controls available in Sheets and Drive
Before migration, the established IDs, transaction history, statuses, ownership rules, approval evidence, license catalog, and exception categories would provide a cleaner foundation for evaluating and configuring a dedicated platform.
Implementation Checklist
- Confirm physical asset, license, maintenance, renewal, and disposal requirements.
- Assign an IT process owner, Finance owner, automation owner, and backup owner.
- Confirm Microsoft Entra ID, Google Workspace, n8n, Slack, and Drive capabilities.
- Create production and test workbooks.
- Create the Entra application registration and grant read-only Graph permissions.
- Create restricted Google and Slack automation credentials.
- Define asset, transaction, license, snapshot, task, run, error, exemption, and lookup structures.
- Define unique IDs and deterministic idempotency keys.
- Build authenticated receipt, movement, maintenance, and approval intake forms.
- Configure conditional fields, validation, duplicate checks, and privacy notices.
- Create protected system-of-record sheets and append-only history sheets.
- Map Entra user and license fields to snapshot fields.
- Configure complete Graph pagination and retry behavior.
- Build asset receipt, custody, maintenance, reconciliation, renewal, and verification workflows.
- Define task ownership, approval thresholds, delegation, rejection, and reassignment.
- Configure reminders, overdue escalation, and unavailable-approver handling.
- Create restricted Drive folders, naming rules, document links, and retention controls.
- Configure Slack notifications and store returned message identifiers.
- Create operational views for new, incomplete, overdue, rejected, exceptional, and failed records.
- Protect sensitive finance, identity, and disposal fields.
- Store credentials outside code, Sheets, and Slack.
- Deploy the complete license-comparison Code node and validate every output field.
- Test normal, duplicate, invalid, unauthorized, failed, retried, and completed scenarios.
- Run Entra reconciliation in report-only mode before creating live tasks.
- Complete user acceptance testing and phased deployment.
- Document cost, volume, time, exception, and maintenance assumptions.
- Measure actual handling time after launch and update the savings model.
- Add AI only after the core workflow is reliable and data use is approved.
- Validate AI output, require human review, and maintain a non-AI fallback.
- Schedule permission, credential, backup, integration, cost, and output reviews.
- Define volume, security, audit, workflow, and maintenance criteria for moving to dedicated software.
Get a FREE
Proof of Concept
& Consultation
No Cost, No Commitment!


