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.

  1. Finance approved a purchase and stored the supplier invoice in its normal accounting records.
  2. 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.
  3. Equipment assignment was communicated through Slack or email. The asset spreadsheet was updated when the IT employee remembered to do so.
  4. Check-ins, temporary loans, maintenance, and location changes were recorded inconsistently. Some appeared in the spreadsheet, while others existed only in messages.
  5. Employee departures generated a separate checklist. IT reviewed Entra ID manually and attempted to identify software that should be removed.
  6. Before a software renewal, Finance requested a current user count. IT exported users or inspected assignments and compared the information manually.
  7. 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.

Business and technical requirements
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

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.

Selected tools and responsibilities
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
  1. 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.
  2. 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.
  3. 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.
  4. A transaction record is appended. Every state-changing event is written to the append-only Asset_Transactions sheet. The current Assets row and historical transaction remain linked by Asset ID.
  5. 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.
  6. 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.
  7. 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.
  8. 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.
  9. 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.
  10. 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.
  11. 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

Important fields in the Assets sheet
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 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

Important fields in the Asset_Transactions sheet
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 Email Authenticated form respondent.
From_Custodian Email Previous custodian where applicable.
To_Custodian Email 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

License, snapshot, task, and run entities
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

Asset lifecycle status and ownership
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

Task workflow and ownership
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.

  1. 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.
  2. Create a test workbook. Copy the tab structure without real employee, device, cost, or supplier data. Use synthetic users and assets during development.
  3. 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.
  4. 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.
  5. Grant Microsoft Graph application permissions. Use User.Read.All for user records, LicenseAssignment.Read.All for license assignment information, and AuditLog.Read.All when last sign-in activity is required. Administrative consent is required. Confirm current Microsoft documentation before deployment because permission requirements can change.
  6. Confirm sign-in activity availability. The signInActivity property 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.
  7. 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.
  8. Configure n8n roles. Limit workflow editing and credential use to automation administrators. Operational users can receive task links without receiving n8n editor access.
  9. 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.
  10. 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.

Intake forms and required fields
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:

  • Assign required the new custodian email.
  • Check Out required both custodian and return date.
  • Check In required the observed condition.
  • Maintenance Open required issue description and target date.
  • Retire Request required retirement reason and data-bearing-device confirmation.
  • Approve Renewal required a new renewal date and evidence reference.
  • Reject required 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_ACCOUNT
  • RENEW:SKU_GUID:2026-11-30
  • RETURN:AST-2026-00184:2026-08-15
  • DISPOSAL: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

Integration connections
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

Microsoft Graph to Google Sheets 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:

  1. Create a run log row with status Running.
  2. Build Source_Event_ID from form and response row.
  3. Search the transaction sheet for that source event.
  4. If found, return the existing transaction and mark the form row Duplicate Event Ignored.
  5. Normalize serial number, asset tag, email, dates, and currency.
  6. Search active assets for matching serial number and asset tag.
  7. If a conflict exists, write an Errors row and stop before changing the asset master.
  8. Generate the Asset ID and Transaction ID.
  9. Create the asset’s Drive folder.
  10. Append the receipt transaction.
  11. Append the asset master row with status Received.
  12. Write returned folder and row identifiers.
  13. Post the inspection task to Slack if action remains.
  14. 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

  1. The department owner received a review task containing user, SKU, reason, last available sign-in date, and due date.
  2. The owner selected Keep, Remove, Reassign, or Need Information.
  3. Keep required a reason and, for temporary conditions such as leave, an exemption expiry date.
  4. Remove moved the task to Approved for Removal.
  5. IT removed the license manually in Entra ID and recorded the action time.
  6. 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

  1. IT submitted a retirement request with reason, condition, and data-bearing-device indicator.
  2. The IT manager confirmed backup requirements, account removal, encryption-key handling, and planned wipe or destruction evidence.
  3. After IT approval, Finance reviewed cost, age, residual value, and write-off information.
  4. Only after both approvals could the asset move to Approved for disposal.
  5. Final status became Disposed only after disposal date, method, vendor or internal method, and evidence link were recorded.
Reminder and escalation rules
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.

Operational views
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

  1. Build all workflows against the test workbook and test Slack channel.
  2. Use synthetic users, licenses, assets, serial numbers, invoices, and approval responses.
  3. 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.
  4. Load a controlled sample of 20 assets and two license SKUs.
  5. Run user acceptance testing with the IT support coordinator, IT manager, procurement analyst, controller, and two department managers.
  6. Correct mappings and document every accepted exception.
  7. Import the physical asset baseline in batches. Reconcile each batch against purchase and custody evidence.
  8. Activate physical-asset intake first.
  9. Activate reminders and disposal approvals second.
  10. Run Entra reconciliation in report-only mode for two cycles without sending manager notifications.
  11. Review false positives and create valid exemptions.
  12. Enable license task creation after the report-only review.
  13. 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

  1. Use a filter to retain records where Record_Type equals Assignment.
  2. Append all assignment records to User_License_Snapshot.
  3. Create a second branch where Candidate equals TRUE.
  4. For each candidate, search Tasks by Task_Idempotency_Key.
  5. If no row exists, generate a Task ID, append the task, post to Slack, and write the returned Slack timestamp.
  6. If an open row exists, update its last-seen run and due-state fields without posting a duplicate initial message.
  7. If a closed row exists and the same condition recurs, route it to manual review before reopening it.
  8. 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 scenarios and recovery
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.

  1. IT submitted the Asset Movement form with event type Check In, asset ID AST-2026-00184, observed condition Fair, destination IT Storage, and a note that the battery was draining unusually quickly.
  2. Google Forms wrote response row 1842. n8n created source event ID MOVE:1842 and confirmed that it had not been processed.
  3. n8n read the asset master. The previous status was Assigned, and the custodian matched Morgan’s email, so the transition was valid.
  4. The workflow generated transaction ID TXN-20260715-01842 and appended the event to Asset_Transactions.
  5. The asset master changed to status Maintenance, location IT Storage, blank custodian, condition Fair, and last transaction TXN-20260715-01842.
  6. A maintenance task was created for IT support with a five-business-day target. A Slack message referenced the task and asset ID.
  7. 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.
  8. 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.
  9. n8n found no existing task with that key and created a normal-priority license review assigned to the department manager.
  10. The manager confirmed that Morgan no longer needed the planning license. The task moved to Approved for Removal.
  11. IT removed the license manually in Entra ID and recorded the action time. The automation did not have write permission to perform removal.
  12. 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.
  13. The laptop repair was completed separately. IT recorded the replacement battery, repair evidence link, condition Good, and status Available.

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.

Representative one-time implementation cost
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
Representative recurring monthly cost
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

Representative savings calculation
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 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

Deterministic and human-controlled decisions
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.

Representative incremental AI value
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.

Functional, security, and reliability tests
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

Maintenance schedule
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!