[{"content":"","date":"23 September 2026","externalUrl":null,"permalink":"/tags/best-practices/","section":"Tags","summary":"","title":"Best Practices","type":"tags"},{"content":"","date":"23 September 2026","externalUrl":null,"permalink":"/tags/date-and-time/","section":"Tags","summary":"","title":"Date and Time","type":"tags"},{"content":"","date":"23 September 2026","externalUrl":null,"permalink":"/tags/developer-tools/","section":"Tags","summary":"","title":"Developer Tools","type":"tags"},{"content":"","date":"23 September 2026","externalUrl":null,"permalink":"/tags/governance/","section":"Tags","summary":"","title":"Governance","type":"tags"},{"content":"Practical notes and guides covering Power Platform, Microsoft 365, automation and related technologies.\n","date":"23 September 2026","externalUrl":null,"permalink":"/guides/","section":"Guides","summary":"","title":"Guides","type":"guides"},{"content":" Engineering practical solutions with Microsoft technology # I specialise in building secure, governed solutions using Microsoft Power Platform and Microsoft 365, with a focus on automation, operational improvement and making complex processes easier for people to use.\nMy work spans Power Apps, Power Automate, SharePoint, Microsoft Graph, Dataverse, Azure Automation and Copilot, bringing these technologies together to solve real business problems.\nThis site is a collection of selected projects, technical approaches and things I\u0026rsquo;ve learned along the way.\nWeekly updates Stay up to date A short weekly digest of new Microsoft 365, Power Platform and Azure guides, project write-ups and automation notes. Email address Subscribe One email per week at most. No spam. Unsubscribe anytime. Powered by Buttondown Almost there Check your inbox I've sent you a confirmation email. Click the confirmation link to finish subscribing to the weekly updates. Can't see it? Check your junk or spam folder. You're in You're subscribed! Thanks for subscribing. You'll receive at most one weekly update when I publish new guides or project write-ups. ","date":"23 September 2026","externalUrl":null,"permalink":"/","section":"Home","summary":"","title":"Home","type":"page"},{"content":"Sometimes you need to give a person or team access to one specific folder in a SharePoint document library, without giving them access to everything else in that library.\nSharePoint supports this through unique permissions.\nThe important thing is to understand permission inheritance before changing anything.\nHow SharePoint Permissions Normally Work # By default, permissions flow down through SharePoint:\nSite → Document Library → Folder → File\nFor example:\nSharePoint Site └── Documents Library ├── General ├── Finance └── Project Alpha Normally, all three folders inherit their permissions from the Documents library.\nIf we want somebody to access Project Alpha without giving them access to the whole library, we can give that folder its own permissions.\nThis is known as breaking permission inheritance.\nOption 1 - Give Someone Access Only to a Folder # This is the most common scenario.\nSuppose:\nSarah does not currently have access to the site or document library Sarah needs access to the Project Alpha folder Sarah should not be able to browse the rest of the library Step 1 - Find the Folder # Open the document library and locate the folder you want to share.\nFor example:\nDocuments └── Project Alpha Select the folder and choose:\nManage access\nDepending on the SharePoint interface, you may also see options such as:\nShare\nor:\nManage access → Advanced\nStep 2 - Grant Access to the Folder # Add the user or security group that requires access.\nChoose the appropriate permission level, normally:\nCan view – read-only access Can edit – allows files to be added, modified and deleted Use the lowest permission level required.\nFor example:\nSG-Project-Alpha-Members → Edit rather than assigning access directly to ten individual users.\nSharePoint can give the user access to the folder without granting them general access to the entire document library. SharePoint may show the user as having Limited Access higher in the site hierarchy; this exists so they can reach the item that was explicitly shared with them and does not mean they have normal access to the rest of the library.\nOption 2 - Make the Folder Private # There is an important difference between:\nGiving someone additional access to a folder\nand:\nMaking sure only a specific group can access the folder.\nIf the folder currently inherits permissions from the library, existing site members may already have access.\nFor example:\nDocuments Library Members → Edit Owners → Full Control If Project Alpha inherits these permissions, adding another user does not remove the existing permissions.\nTo make the folder restricted, you need to create unique permissions.\nStep 1 - Stop Inheriting Permissions # Open:\nFolder → Manage access → Advanced\nThen select:\nStop Inheriting Permissions\nSharePoint copies the existing permissions onto the folder and then disconnects the folder from the parent library.\nThe folder now has its own permission scope.\nStep 2 - Remove Unwanted Access # After breaking inheritance, review the permissions carefully.\nFor example, you might initially see:\nSite Owners Full Control Site Members Edit Site Visitors Read If the folder is supposed to be restricted, remove the groups that should not have access.\nThen add the appropriate group:\nSG-Project-Alpha-Members Edit The resulting permissions might look like:\nProject Alpha ├── SG-Project-Alpha-Members Edit └── Approved administrators Full Control The rest of the library can continue using its normal permissions.\nWhat Happens to Files Inside the Folder? # Files and subfolders normally inherit permissions from their parent folder.\nThat means a structure such as:\nProject Alpha ├── Requirements.docx ├── Budget.xlsx └── Documentation └── Architecture.docx can normally be secured with one unique permission scope on Project Alpha.\nYou do not need to configure every document individually.\nThis is both easier to manage and better for SharePoint performance. Microsoft recommends using folders to group content requiring the same permissions rather than assigning unique permissions to thousands of individual files.\nCheck for Existing Unique Permissions # There is one important exception.\nA file or subfolder may already have been shared separately.\nFor example:\nProject Alpha ├── Requirements.docx ├── Budget.xlsx ← previously shared separately └── Documentation Budget.xlsx may already have its own permissions.\nChanging the permissions on Project Alpha does not necessarily remove permissions that have already been configured independently on child items.\nIf the folder contains sensitive information, check whether any files or subfolders already have unique permissions.\nUse Groups Instead of Individual Users # Where possible, avoid permissions such as:\nJohn Smith Jane Smith Alex Jones Mary Brown Instead, use a security group:\nSG-Project-Alpha-Members Then manage membership of the group rather than repeatedly modifying SharePoint permissions.\nThis becomes significantly easier when people join or leave the project.\nFor example:\nSG-Project-Alpha-Members ├── John ├── Jane ├── Alex └── Mary When Alex leaves the project, remove Alex from the group instead of finding every SharePoint location where access was assigned.\nAvoid Unique Permissions on Individual Files # Technically, SharePoint allows unique permissions on individual documents.\nThat does not mean it is always a good idea.\nFor example, avoid structures like:\nDocuments ├── File01.docx → unique permissions ├── File02.docx → unique permissions ├── File03.docx → unique permissions ├── File04.docx → unique permissions └── ... Permissions quickly become difficult to understand and troubleshoot.\nA cleaner design is:\nDocuments └── Restricted Project ├── File01.docx ├── File02.docx ├── File03.docx └── File04.docx and apply unique permissions once to the Restricted Project folder.\nMicrosoft supports up to 50,000 unique permission scopes in a list or library, but recommends keeping the number below 5,000 for best performance.\nBe Careful with Very Large Folders # There is also an important SharePoint limit.\nIf a folder contains more than 100,000 items, SharePoint cannot break or restore permission inheritance on that folder.\nIf you know a large folder will require unique permissions, configure the permissions early rather than waiting until the folder contains a very large number of files.\nCommon Mistakes # Granting access to the whole library # If somebody only needs one folder, do not add them to a site or library-level group unless they genuinely need access to everything.\nBreaking inheritance unnecessarily # Not every folder needs unique permissions.\nKeep normal inheritance wherever possible and create unique permissions only where there is a genuine access requirement.\nForgetting existing permissions # Breaking inheritance initially copies the existing permissions.\nYou still need to remove users or groups that should no longer have access.\nManaging dozens of individual users # Use Entra ID security groups or appropriate SharePoint groups where possible.\nIt makes access reviews and staff changes much easier.\nSetting unique permissions on every file # Secure the highest sensible container.\nUsually:\nFolder ↓ Files inherit from folder is preferable to:\nFile → unique permissions File → unique permissions File → unique permissions Recommended Permission Structure # A simple design might look like this:\nDocuments Library │ ├── General │ └── Inherits library permissions │ ├── Team Documents │ └── Inherits library permissions │ └── Restricted Project │ ├── Unique permissions │ ├── SG-Restricted-Project-Members → Edit └── Files and subfolders inherit from this folder This keeps the majority of the library using standard inheritance while creating an isolated permission boundary only where it is required.\nBest-Practice Checklist # Before granting folder-level access, check:\nDoes the user really need access to the whole library? Can access be assigned to a security group instead of individual users? Can the files requiring the same permissions be placed in one folder? Are there already unique permissions on files inside the folder? Have inherited groups been removed if the folder needs to be private? Are users receiving the minimum permissions they require? Can normal permission inheritance be retained everywhere else? Final Recommendation # Use folder-level permissions when there is a clear business requirement, but keep the permission structure as simple as possible.\nA good SharePoint design normally follows this principle:\nUse inheritance by default ↓ Create unique permissions only where required ↓ Assign permissions to groups ↓ Allow files to inherit from the secured folder This provides granular access without turning the document library into a collection of hundreds or thousands of individually managed permission scopes.\n","date":"23 September 2026","externalUrl":null,"permalink":"/guides/grant-access-to-sp-folder-not-whole-library/","section":"Guides","summary":"","title":"How to Grant Access to One SharePoint Folder Without Granting Access to the Whole Library","type":"guides"},{"content":"","date":"23 September 2026","externalUrl":null,"permalink":"/tags/microsoft-365/","section":"Tags","summary":"","title":"Microsoft 365","type":"tags"},{"content":"","date":"23 September 2026","externalUrl":null,"permalink":"/tags/permissions/","section":"Tags","summary":"","title":"Permissions","type":"tags"},{"content":"","date":"23 September 2026","externalUrl":null,"permalink":"/tags/power-automate/","section":"Tags","summary":"","title":"Power Automate","type":"tags"},{"content":"Power Automate occasionally requires time values to be expressed as ticks rather than familiar units such as hours or days.\nThis small calculator makes those conversions easier and also shows the calculation behind the result.\nInteractive tool\nTicks Calculator Convert practical time values into .NET ticks, or convert an existing tick value back into readable time. 1 second = 10,000,000 ticks Time → Ticks Ticks → Time Value Unit Seconds Minutes Hours Days Weeks Quick presets 1 hour 8 hours 1 day 7 days 30 days 90 days Tick value 36,000,000,000 Copy ticks Power Automate expression addTicks(utcNow(), 36000000000) Copy expression Tick value Commas and spaces are accepted.\nSeconds 86,400 Minutes 1,440 Hours 24 Days 1 Weeks 0.142857 JavaScript is required for the interactive calculator. What is a tick? # A tick is a unit of time used by .NET and several Microsoft technologies.\n1 tick = 100 nanoseconds 10,000,000 ticks = 1 second That means:\nDuration Ticks 1 second 10,000,000 1 minute 600,000,000 1 hour 36,000,000,000 1 day 864,000,000,000 1 week 6,048,000,000,000 Example in Power Automate # To add one day to the current time using ticks:\naddTicks(utcNow(), 864000000000) To subtract one day:\naddTicks(utcNow(), -864000000000) The calculator above can generate the tick value for a custom duration and gives you a ready-to-copy Power Automate expression.\nWhy use this? # Ticks can be useful when you need precise date and time calculations or when working with expressions and APIs that represent time using .NET tick values.\nFor most ordinary date arithmetic, functions such as addDays(), addHours() and addMinutes() are easier to read. This tool is intended for the cases where ticks are useful or required.\n","date":"23 September 2026","externalUrl":null,"permalink":"/tools/ticks-calculator/","section":"Tools","summary":"Convert practical time values to .NET ticks, reverse ticks back into readable time, and copy ready-to-use Power Automate expressions.","title":"Power Automate Ticks Calculator","type":"tools"},{"content":"","date":"23 September 2026","externalUrl":null,"permalink":"/tags/power-platform/","section":"Tags","summary":"","title":"Power Platform","type":"tags"},{"content":"","date":"23 September 2026","externalUrl":null,"permalink":"/tags/security/","section":"Tags","summary":"","title":"Security","type":"tags"},{"content":"","date":"23 September 2026","externalUrl":null,"permalink":"/tags/sharepoint/","section":"Tags","summary":"","title":"SharePoint","type":"tags"},{"content":"","date":"23 September 2026","externalUrl":null,"permalink":"/tags/sharepoint-online/","section":"Tags","summary":"","title":"SharePoint Online","type":"tags"},{"content":"When creating a new SharePoint site, one of the first decisions is choosing the right type of site.\nAt a high level, modern SharePoint is built around two main site types:\nTeam sites – for collaboration Communication sites – for publishing and sharing information There are also several important variations and architectural concepts, such as hub sites, Teams channel sites and home sites.\nThis quick guide explains what each one is, when to use it, and the main advantages and disadvantages.\nQuick Comparison # Site type Best for Typical audience Main purpose Team site Teams, projects and working groups Small/medium group Collaboration Communication site Departments, portals and information sites Large audience Publishing Hub site Connecting related SharePoint sites Organisation-wide Navigation and organisation Teams channel site Private/shared Teams channels Specific channel members Restricted collaboration Home site Main organisational intranet Whole organisation Central landing page Classic site Legacy SharePoint solutions Existing users Legacy compatibility Microsoft describes Team, Communication and Hub sites as the main building blocks of a modern SharePoint intranet.\n1. Team Site # Best for # Use a Team site when a group of people need to actively work together.\nExamples include:\nProject teams Engineering teams Working groups Operational teams Case management Shared document repositories Power Apps or Power Automate solutions A Team site is primarily designed for collaboration rather than publishing. Members would normally create, edit and work with content rather than simply read it.\nExample # Infrastructure Project │ ├── Documents ├── Project Risks ├── Actions ├── Technical Documentation └── Project Updates Everyone in the project team can work with the content.\nMicrosoft 365 Group-Connected Team Site # When you create a normal modern Team site, it is normally connected to a Microsoft 365 Group.\nThat provides additional Microsoft 365 resources such as:\nMicrosoft 365 Group │ ├── SharePoint Team Site ├── Shared Mailbox ├── Shared Calendar ├── Planner └── Microsoft Teams Microsoft recommends managing membership of these sites through the associated Microsoft 365 Group or Microsoft Teams team.\nPros # Excellent for collaboration Integrates naturally with Microsoft Teams Microsoft 365 Group manages membership Good for document co-authoring Supports SharePoint Lists Works well with Power Automate and Power Apps Easy to expand with Planner and other Microsoft 365 services Cons # Creates additional Microsoft 365 resources Permissions can become confusing if SharePoint permissions and Teams/Group membership are managed separately Usually not ideal for large information portals Members normally have relatively high contribution permissions 2. Team Site Without a Microsoft 365 Group # There is another useful variation: a Team site that is not connected to a Microsoft 365 Group.\nThis can be useful when you need SharePoint functionality without needing:\nTeams Planner A group mailbox A group calendar Other Microsoft 365 Group services For example:\nPower Platform Application Data │ ├── Configuration List ├── Contacts ├── Job Log └── Application Documents A non-group-connected site can be useful for SharePoint-backed applications and solutions where you want more direct control over SharePoint permissions. Microsoft\u0026rsquo;s Microsoft 365 community guidance specifically identifies app data, lists, libraries and scenarios with differing permissions as potential uses.\nPros # Simpler when only SharePoint is required Greater flexibility over traditional SharePoint permissions Good location for application data Useful for Power Apps and Power Automate solutions Avoids unnecessary Microsoft 365 Group resources Cons # No automatic Teams integration Membership must be managed separately No associated group mailbox or Planner Users may find it less obvious where the site belongs organisationally 3. Communication Site # Best for # Use a Communication site when a small number of people publish information to a much larger audience.\nThink:\nFew people create - many people read.\nTypical examples include:\nHR portal IT support portal Department homepage Policies and guidance Internal news Service catalogue Corporate communications Project information portal Microsoft describes Communication sites as being designed for broad communication, where relatively few contributors create content for many readers.\nExample # Technology Department │ ├── Latest News ├── Services ├── Policies ├── Support ├── Projects └── Useful Links Most employees would have Read access while a smaller group maintains the site.\nPros # Excellent for publishing information Designed for visually rich pages Good for large audiences Strong use of News, Hero and Quick Links web parts Straightforward Owners / Members / Visitors permission model Excellent foundation for an intranet Does not automatically create a Microsoft 365 Group Cons # Not designed primarily for day-to-day team collaboration Doesn\u0026rsquo;t automatically provide Teams, Planner or a shared mailbox Requires proper content ownership to prevent information becoming outdated May require additional planning around navigation and publishing Team Site vs Communication Site # A very simple rule is:\nAre people WORKING together? YES ↓ Team Site versus:\nAre a few people PUBLISHING information for many people to consume? YES ↓ Communication Site For example:\nFinance employees collaboratively preparing the annual budget\n→ Team site\nFinance publishing expenses guidance for all employees\n→ Communication site\nThat distinction solves most SharePoint site-selection decisions.\n4. Hub Site # A Hub site isn\u0026rsquo;t really a completely separate site template.\nInstead, an existing Team or Communication site can be registered as a Hub by a SharePoint administrator.\nHub sites are used to connect related SharePoint sites.\nFor example:\nTechnology Hub │ ├── Infrastructure Team ├── Power Platform Team ├── Service Desk ├── Cyber Security └── Architecture Each team can maintain its own SharePoint site while the Hub provides a shared structure.\nWhat a Hub Provides # Hub sites can provide:\nShared navigation Consistent branding Aggregated news Aggregated content Search across related sites Logical organisation of sites Microsoft describes hubs as the connective tissue used to organise families of Team and Communication sites.\nImportantly, joining a site to a Hub does not automatically change the permissions of either site.\nPros # Excellent for organising large SharePoint environments Sites remain independent Provides consistent navigation Helps users discover related sites Can aggregate news and information Avoids creating complicated subsite structures Cons # Requires governance and planning Normally requires SharePoint Administrator involvement Does not automatically standardise permissions Poorly designed hubs can create confusing navigation Not every site necessarily needs to belong to a Hub Avoid Building Everything with Subsites # Older SharePoint environments often looked like this:\nCorporate │ └── Technology │ └── Infrastructure │ └── Cloud Modern SharePoint architecture generally favours separate site collections connected using Hub sites instead.\nMicrosoft recommends hubs rather than subsites for organising modern SharePoint environments.\nA modern structure might therefore look like:\nTechnology Hub │ ├── Infrastructure Site ├── Cloud Site ├── Development Site └── Service Desk Site This makes sites easier to restructure later.\n5. Microsoft Teams Channel Site # Microsoft Teams and SharePoint are closely connected.\nEvery Microsoft Team has a SharePoint Team site behind it.\nFor normal Standard channels, documents are normally stored within folders in the Team\u0026rsquo;s main SharePoint site.\nHowever:\nPrivate channels and Shared channels create separate SharePoint sites.\nFor example:\nMicrosoft Team: Technology Project Main SharePoint Site ├── General ├── Planning └── Documentation Private Channel: Project Leadership ↓ Separate SharePoint Site Best for # These sites are automatically created when Teams requires isolated membership.\nTypical examples:\nLeadership channels Confidential project groups Cross-organisation Shared channels Restricted working groups Pros # Membership follows the Teams channel Provides isolation from the parent Team Automatically created and managed Useful for restricted collaboration Cons # Can significantly increase the number of SharePoint sites Users may not realise that another SharePoint site exists Permissions should generally be managed through Teams rather than directly in SharePoint Can make governance and administration more complicated Best Practice # Avoid manually changing the permissions of Teams channel sites unless you understand the implications.\nManage membership primarily through Microsoft Teams.\n6. SharePoint Home Site # A Home site is a Communication site that has been designated as a central organisational landing page.\nIt can act as the front door to an organisation\u0026rsquo;s SharePoint intranet.\nFor example:\nOrganisation Home │ ├── Corporate News ├── HR ├── Technology ├── Finance ├── Policies ├── Applications └── Useful Links Users can then navigate into different Communication, Team and Hub sites.\nBest for # Corporate intranets Organisation-wide navigation Corporate news Important organisational resources Viva Connections experiences Pros # Provides a recognisable central entry point Good for organisation-wide news Supports global navigation scenarios Integrates with broader intranet architecture Can reinforce organisational branding Cons # Requires considerable content planning Usually needs central ownership Poor navigation design can make the intranet difficult to use Not intended as a replacement for individual Team or Communication sites Think of a Home site as:\nThe front door to your intranet\nrather than the place where every piece of content should live.\n7. Classic SharePoint Sites # You may also encounter Classic SharePoint sites.\nThese are mainly associated with older SharePoint architectures and solutions.\nThey may contain things such as:\nClassic web parts Older publishing functionality Subsites Custom master pages Older SharePoint customisations Microsoft still supports classic functionality in various scenarios, but modern Team and Communication sites are the preferred foundation for new SharePoint Online solutions. Microsoft specifically recommends using Hub sites instead of building new architectures around subsites.\nPros # Supports existing legacy solutions May be required for older SharePoint customisations Organisations may already have significant content built on them Cons # Older user experience More difficult to integrate with modern Microsoft 365 experiences Often relies heavily on subsites Can be more difficult to maintain Generally not the best starting point for a new SharePoint Online solution Best Practice # For a new solution, start with modern SharePoint unless there is a specific technical requirement for classic functionality.\nWhich Site Should I Choose? # Use this simplified decision tree:\nWhat are you trying to build? │ ├── A team or project workspace │ ↓ │ Team Site │ ├── Information portal / department site │ ↓ │ Communication Site │ ├── Collection of related sites │ ↓ │ Hub Site │ ├── Organisation-wide intranet landing page │ ↓ │ Home Site │ ├── Restricted Teams channel │ ↓ │ Teams creates a Channel Site │ └── SharePoint storage for an app/automation ↓ Consider a Team Site without a Microsoft 365 Group Some Practical Examples # Scenario 1 # Ten engineers need somewhere to collaborate on documentation and project files.\nUse: Team site\nScenario 2 # HR needs somewhere to publish policies and employee guidance to 5,000 employees.\nUse: Communication site\nScenario 3 # HR has separate sites for Recruitment, Learning, Payroll and Policies but wants common navigation.\nUse: Hub site\nHR Hub ├── Recruitment ├── Learning ├── Payroll └── Policies Scenario 4 # A Power App needs several SharePoint lists for configuration, logging and reference data.\nConsider: Team site without a Microsoft 365 Group\nThis avoids creating Teams, Planner and other collaboration resources that the application does not require.\nScenario 5 # A Teams project has a private channel that only senior managers should access.\nUse: Private Teams channel\nTeams automatically provisions the associated SharePoint channel site.\nScenario 6 # The organisation needs one main place where employees can find news, services and departmental portals.\nUse: SharePoint Home site, typically supported by Hub and Communication sites underneath it.\nRecommended Modern SharePoint Architecture # A larger organisation might eventually look something like this:\nOrganisation Home │ ┌─────────────────┼─────────────────┐ │ │ │ HR Hub Technology Hub Finance Hub │ │ │ ┌────┼────┐ ┌────┼────┐ ┌────┼────┐ │ │ │ │ │ │ │ │ │ HR HR HR IT IT IT FIN FIN FIN Sites Sites Sites Sites Sites Sites Sites Sites Sites Individual project and collaboration Team sites can then sit alongside this structure where required.\nThe principle is:\nCollaborate → Team Site Communicate → Communication Site Organise → Hub Site Organisation Landing Page → Home Site Best-Practice Checklist # Before creating a SharePoint site, ask:\nWho is the audience? Are users mainly reading or editing content? Does the site need Microsoft Teams? Does it need a Microsoft 365 Group? Is this an independent project or part of a wider department? Should it be associated with an existing Hub? Who will own the site? Who will maintain its content? What should happen to the site when the project finishes? Is another existing site already suitable? Creating a site is easy.\nMaintaining hundreds or thousands of unnecessary SharePoint sites is considerably harder.\nGood SharePoint architecture starts by creating the right site for the right purpose.\n","date":"23 September 2026","externalUrl":null,"permalink":"/guides/sharepoint-site-types-which-one-to-use/","section":"Guides","summary":"","title":"SharePoint Site Types: Which One Should You Use?","type":"guides"},{"content":"","date":"23 September 2026","externalUrl":null,"permalink":"/tags/site-architecture/","section":"Tags","summary":"","title":"Site Architecture","type":"tags"},{"content":"","date":"23 September 2026","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"A growing collection of small interactive tools I have built to make common engineering and automation tasks quicker.\nThey are intentionally lightweight: open the tool, get the value or expression you need, and carry on with the job.\n","date":"23 September 2026","externalUrl":null,"permalink":"/tools/","section":"Tools","summary":"","title":"Tools","type":"tools"},{"content":"","date":"22 September 2026","externalUrl":null,"permalink":"/tags/automation/","section":"Tags","summary":"","title":"Automation","type":"tags"},{"content":"","date":"22 September 2026","externalUrl":null,"permalink":"/tags/excel/","section":"Tags","summary":"","title":"Excel","type":"tags"},{"content":"","date":"22 September 2026","externalUrl":null,"permalink":"/tags/identity/","section":"Tags","summary":"","title":"Identity","type":"tags"},{"content":"","date":"22 September 2026","externalUrl":null,"permalink":"/tags/microsoft-entra-id/","section":"Tags","summary":"","title":"Microsoft Entra ID","type":"tags"},{"content":"","date":"22 September 2026","externalUrl":null,"permalink":"/tags/microsoft-graph/","section":"Tags","summary":"","title":"Microsoft Graph","type":"tags"},{"content":"","date":"22 September 2026","externalUrl":null,"permalink":"/tags/office-scripts/","section":"Tags","summary":"","title":"Office Scripts","type":"tags"},{"content":"","date":"22 September 2026","externalUrl":null,"permalink":"/tags/power-apps/","section":"Tags","summary":"","title":"Power Apps","type":"tags"},{"content":"Running an Office Script from Power Automate is useful on its own, but it becomes much more powerful when the script can send information back to the flow.\nFor example, an Office Script could process an Excel workbook and return:\nhow many rows were processed; whether the operation succeeded; the name of a worksheet that was created; a calculated total; a warning message; or several pieces of information together. Power Automate can then use that result elsewhere in the workflow.\nAnd if the flow was started from Power Apps, the same information can also be sent back to the app.\nThe overall pattern looks like this:\nOffice Script │ ▼ Power Automate │ ├── Continue processing │ └── Return result to Power Apps Let\u0026rsquo;s look at both scenarios.\n1. Returning a Simple Value from an Office Script # The easiest example is returning a single value.\nSuppose an Office Script counts the number of rows in an Excel table.\nfunction main(workbook: ExcelScript.Workbook): number { const table = workbook.getTable(\u0026#34;Users\u0026#34;); const rowCount = table.getRangeBetweenHeaderAndTotal() .getRowCount(); return rowCount; } The important part is:\n: number after the main() function.\nThis tells Office Scripts that the function will return a number.\nThen:\nreturn rowCount; actually sends the value back.\nMicrosoft supports returning data from an Office Script into the Run script action in Power Automate. The returned value appears as dynamic content called result.\n2. Getting the Result in Power Automate # Your flow might look like:\nTrigger ↓ Run script ↓ Compose ↓ Continue processing After adding the Run script action, select your workbook and Office Script.\nOnce Power Automate recognises that the script returns a value, you should see:\nresult available in Dynamic content.\nFor our example, that result might be:\n428 You could then use it in an email:\n428 rows were processed. or in a condition:\nIs result greater than 0? or store it somewhere else.\nReturning Text # The same concept works with text.\nFor example:\nfunction main(workbook: ExcelScript.Workbook): string { const worksheet = workbook.getActiveWorksheet(); return worksheet.getName(); } If the active worksheet is:\nSeptember Report Power Automate receives:\nSeptember Report as the script result.\nReturning True or False # You can also return a Boolean value.\nfunction main(workbook: ExcelScript.Workbook): boolean { const worksheet = workbook.getWorksheet(\u0026#34;Report\u0026#34;); return worksheet !== undefined; } Power Automate can then use the returned result in a condition.\nConceptually:\nRun script ↓ Did script return true? / \\ Yes No This can be useful when the Office Script is performing validation before the rest of the flow continues.\n3. Returning Multiple Values # In real automations, returning just one value often isn\u0026rsquo;t enough.\nImagine a script that cleans an Excel report.\nAt the end you might want to know:\nWas it successful? How many rows were processed? What worksheet was used? Was there a warning? Instead of trying to return several individual values, you can return an object.\nFor example:\ninterface ScriptResult { success: boolean; rowsProcessed: number; worksheetName: string; message: string; } function main( workbook: ExcelScript.Workbook ): ScriptResult { const worksheet = workbook.getWorksheet(\u0026#34;Report\u0026#34;); const usedRange = worksheet.getUsedRange(); const rowsProcessed = usedRange.getRowCount(); return { success: true, rowsProcessed: rowsProcessed, worksheetName: worksheet.getName(), message: \u0026#34;Report processed successfully\u0026#34; }; } Now instead of returning:\n428 the script effectively returns something like:\n{ \u0026#34;success\u0026#34;: true, \u0026#34;rowsProcessed\u0026#34;: 428, \u0026#34;worksheetName\u0026#34;: \u0026#34;Report\u0026#34;, \u0026#34;message\u0026#34;: \u0026#34;Report processed successfully\u0026#34; } This is much more useful for larger automations.\nOffice Scripts supports returning objects to Power Automate, provided the return type is declared on the main() function.\nWhy I Prefer Returning an Object # For anything beyond a very simple script, I normally prefer something like:\ninterface ScriptResult { success: boolean; message: string; rowsProcessed: number; } rather than returning only:\ntrue Why?\nBecause:\ntrue only tells Power Automate:\nSomething succeeded.\nWhereas:\n{ \u0026#34;success\u0026#34;: true, \u0026#34;message\u0026#34;: \u0026#34;Import completed successfully\u0026#34;, \u0026#34;rowsProcessed\u0026#34;: 754 } actually gives the flow useful information.\nIt also makes troubleshooting much easier.\nA Practical Example # Imagine a flow that receives a new Excel report every morning.\nThe automation could be:\nNew report arrives ↓ Save report to SharePoint ↓ Run Office Script ↓ Script cleans workbook ↓ Script returns result ↓ Power Automate checks result ↓ Send notification The Office Script might return:\n{ \u0026#34;success\u0026#34;: true, \u0026#34;rowsProcessed\u0026#34;: 1257, \u0026#34;message\u0026#34;: \u0026#34;Daily report processed successfully\u0026#34; } Power Automate could then send:\nDaily report processed successfully. Rows processed: 1,257 This is much cleaner than making Power Automate rediscover information the script already knows.\nA Useful Pattern: Return a Status Object # For scripts I expect to use in Power Automate, I like using a consistent response structure.\nFor example:\ninterface ScriptResult { success: boolean; message: string; rowsProcessed: number; } Then the script returns something similar to:\nreturn { success: true, message: \u0026#34;Processing completed successfully.\u0026#34;, rowsProcessed: 428 }; Your Power Automate flow can then make decisions based on the returned information.\nConceptually:\nRun Office Script ↓ Check success / \\ true false ↓ ↓ Continue Handle failure Important: Changing the Script Return Type # There is one slightly unintuitive behaviour worth knowing.\nPower Automate reads the parameters and return type of your Office Script when you add the Run script action.\nIf you later change:\nfunction main(workbook: ExcelScript.Workbook) to:\nfunction main( workbook: ExcelScript.Workbook ): ScriptResult Power Automate might not immediately recognise the new output.\nMicrosoft recommends removing and recreating the Run script action if the script\u0026rsquo;s parameters or return type have changed.\nSo if your new result mysteriously doesn\u0026rsquo;t appear in Dynamic content, this is one of the first things to check.\n4. Returning the Result to Power Apps # Things become particularly interesting when Power Apps starts the flow.\nThe architecture becomes:\nPower Apps │ │ .Run() ▼ Power Automate │ ▼ Run Office Script │ ▼ Office Script returns result │ ▼ Power Automate │ ▼ Respond to a Power App or flow │ ▼ Power Apps receives result For a canvas app, Power Automate provides a convenient bridge between the app and the Office Script.\nPower Apps can trigger a cloud flow using the flow\u0026rsquo;s .Run() function.\nExample Scenario # Imagine a Power App where the user selects:\nProcess Report The app calls a Power Automate flow.\nThe flow runs an Office Script against an Excel workbook.\nThe Office Script processes the workbook and returns:\n{ \u0026#34;success\u0026#34;: true, \u0026#34;rowsProcessed\u0026#34;: 428, \u0026#34;message\u0026#34;: \u0026#34;Report processed successfully.\u0026#34; } The flow then sends those values back to Power Apps.\nStep 1 - Power Apps Calls the Flow # A button\u0026rsquo;s OnSelect property could contain something like:\nSet( varResult, ProcessExcelReport.Run() ) The important part is:\nProcessExcelReport.Run() which starts the Power Automate flow.\nWrapping it inside:\nSet(...) allows us to keep the response returned by the flow.\nStep 2 - Power Automate Runs the Office Script # Inside the flow:\nPower Apps (V2) ↓ Run script ↓ Respond to a Power App or flow The Office Script returns our object.\nFor example:\n{ \u0026#34;success\u0026#34;: true, \u0026#34;rowsProcessed\u0026#34;: 428, \u0026#34;message\u0026#34;: \u0026#34;Report processed successfully.\u0026#34; } Step 3 - Add \u0026ldquo;Respond to a Power App or flow\u0026rdquo; # At the end of the flow, add:\nRespond to a Power App or flow\nThis action allows the flow to send values back to the calling app. Microsoft also documents this response action as the mechanism for defining outputs returned to a caller.\nYou could create outputs such as:\nSuccess Message RowsProcessed Then map them to the values returned by the Office Script.\nFor example:\nSuccess → script result success Message → script result message RowsProcessed → script result rowsProcessed Step 4 - Use the Result in Power Apps # Back in Power Apps:\nSet( varResult, ProcessExcelReport.Run() ) varResult now represents the response from the flow.\nYou could display:\nvarResult.message in a label.\nOr:\nvarResult.rowsprocessed to display how many rows were processed.\nShowing a Notification # You could also display the returned message directly to the user.\nFor example:\nSet( varResult, ProcessExcelReport.Run() ); Notify( varResult.message, NotificationType.Success ) The experience becomes:\nUser presses button ↓ Power App calls Flow ↓ Flow runs Office Script ↓ Script processes workbook ↓ Script returns result ↓ Flow returns result to app ↓ \u0026#34;Report processed successfully.\u0026#34; That can make the application feel much more interactive.\nReturn More Than Just \u0026ldquo;Success\u0026rdquo; # A common mistake is returning only:\nSuccess or:\nFailed I prefer returning enough information for the app to understand what happened.\nFor example:\nSuccess: true RowsProcessed: 428 Message: Report processed successfully Then the Power App can decide how it wants to display the result.\nFor example:\nIf( varResult.success, Notify( varResult.message, NotificationType.Success ), Notify( varResult.message, NotificationType.Error ) ) This keeps the responsibilities nicely separated.\nThe Office Script understands Excel.\nPower Automate understands the workflow.\nPower Apps understands the user experience.\nPassing Data Both Ways # You can also send information into the Office Script and then receive something back.\nFor example:\nPower Apps │ │ Report name ▼ Power Automate │ ▼ Office Script │ │ Processing result ▼ Power Automate │ ▼ Power Apps Suppose the user enters a department:\nFinance Power Apps sends:\nFinance to Power Automate.\nPower Automate passes it to the Office Script:\nfunction main( workbook: ExcelScript.Workbook, department: string ): string { // Process workbook... return `Report processed for ${department}`; } The result returned to Power Automate becomes:\nReport processed for Finance and that can then be passed back to Power Apps.\nOffice Script parameters are exposed as inputs on the Power Automate Run script action, while script return values appear as its result.\nA Realistic Architecture # A larger solution might look like this:\n┌─────────────────┐ │ Power Apps │ │ │ │ Select report │ │ Press Process │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ Power Automate │ │ │ │ Validate input │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ Office Script │ │ │ │ Read workbook │ │ Process rows │ │ Update Excel │ │ Calculate result│ └────────┬────────┘ │ │ │ Result ▼ ┌─────────────────┐ │ Power Automate │ │ │ │ Check result │ │ Log operation │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ Power Apps │ │ │ │ Show result │ └─────────────────┘ This gives each technology a clear job.\nDon\u0026rsquo;t Make Power Apps Wait for Long Processes # There is one important consideration when a Power App is waiting for a response.\nFlows that need to respond to a calling app operate within an inbound request response window. Microsoft currently documents a 120-second limit for these synchronous request/response scenarios.\nSo this pattern is ideal for operations such as:\nValidate workbook Calculate value Process a reasonable dataset Return a status It is less suitable for:\nProcess a huge workbook Wait for an approval Wait several minutes Perform a very long-running batch operation For longer operations, it is usually better to start the job and let the app check its status separately rather than keeping the user waiting for a response.\nTroubleshooting: My Result Isn\u0026rsquo;t Appearing # If Power Automate doesn\u0026rsquo;t show the script\u0026rsquo;s returned value, check these first.\n1. Does main() declare a return type? # Instead of:\nfunction main(workbook: ExcelScript.Workbook) you may need:\nfunction main( workbook: ExcelScript.Workbook ): string or:\nfunction main( workbook: ExcelScript.Workbook ): ScriptResult 2. Does the script actually return something? # For example:\nreturn result; 3. Did you change the script after adding \u0026ldquo;Run script\u0026rdquo;? # If you changed the function parameters or return type, remove the existing Run script action and add it again.\nPower Automate stores the script signature when the action is created, so an old action may not recognise your new outputs.\n4. Check the Flow Run History # Open the completed flow run and inspect:\nRun script ↓ Outputs This is often the quickest way to see exactly what the script returned.\nWhich Pattern Should I Use? # For a simple Power Automate flow:\nOffice Script ↓ return number/string ↓ Power Automate For a more complex flow:\nOffice Script ↓ return object ↓ Power Automate ↓ Use properties from result For Power Apps:\nPower Apps ↓ Power Automate ↓ Office Script ↓ return object ↓ Power Automate ↓ Respond to a Power App or flow ↓ Power Apps Final Takeaway # The most important concept is that an Office Script doesn\u0026rsquo;t have to simply modify a workbook and finish.\nIt can also tell the rest of your solution what happened.\nInstead of:\nRun script ↓ Hope everything worked you can build:\nRun script ↓ Return: - success - message - rows processed - calculated values ↓ Power Automate makes a decision ↓ Power Apps shows the result For small scripts, returning a single value may be enough.\nFor larger automations, returning a structured result object gives Power Automate much more useful information and provides a clean way of passing those results all the way back to Power Apps.\nA useful rule to remember is:\nOffice Scripts process the workbook, Power Automate handles the workflow, and Power Apps presents the result to the user.\n","date":"22 September 2026","externalUrl":null,"permalink":"/guides/returning-data-from-office-script-to-pa/","section":"Guides","summary":"","title":"Returning Data from Office Scripts to Power Automate and Power Apps","type":"guides"},{"content":"","date":"22 September 2026","externalUrl":null,"permalink":"/tags/typescript/","section":"Tags","summary":"","title":"TypeScript","type":"tags"},{"content":"When investigating inactive accounts in Microsoft Entra ID, one of the first questions is usually:\nWhen did this user last sign in?\nUnfortunately, the answer is not always as simple as looking at a single date.\nMicrosoft Entra records several different types of sign-in activity, and they represent different things. It is therefore possible to see a user with a Last interactive sign-in from February while Microsoft Graph reports their Last successful sign-in as November.\nBoth values can be correct.\nUnderstanding the difference is particularly important when building automated processes that identify inactive users, remove licences, disable accounts or trigger account deletion.\nThe main sign-in timestamps # Microsoft Graph exposes sign-in information through the user\u0026rsquo;s signInActivity property.\nThe three most useful timestamps are:\nProperty What it represents Includes failures? lastSignInDateTime Most recent interactive sign-in attempt Yes lastNonInteractiveSignInDateTime Most recent non-interactive sign-in attempt Yes lastSuccessfulSignInDateTime Most recent successful interactive or non-interactive sign-in No Microsoft specifically recommends lastSuccessfulSignInDateTime when you need to determine when an account was actually accessed.\nThat distinction is very important.\n1. Last interactive sign-in # The Graph property is:\nlastSignInDateTime An interactive sign-in is one where the user actively participates in authentication.\nExamples include signing in with:\na username and password MFA Microsoft Authenticator biometric authentication another interactive authentication method However, lastSignInDateTime does not mean the authentication succeeded.\nMicrosoft documents this value as the most recent interactive attempt, whether successful or unsuccessful.\nFor example:\nLast successful sign-in: 24 November Last interactive sign-in: 2 February At first glance this looks wrong.\nBut the February event could simply have been:\n2 February User attempts to sign in ↓ Password incorrect / Conditional Access blocks access / account disabled ↓ Interactive sign-in timestamp is updated ↓ Successful sign-in timestamp remains 24 November This is why lastSignInDateTime should not be treated as proof that the user actually accessed the environment.\n2. Last non-interactive sign-in # The Graph property is:\nlastNonInteractiveSignInDateTime Non-interactive authentication happens without the user being directly prompted to authenticate again.\nThis commonly occurs when an application obtains another access token on behalf of an already authenticated user.\nExamples include:\nOutlook refreshing authentication Teams requesting another access token an Office application using an existing session Single Sign-On refresh-token activity A user could therefore generate non-interactive activity without manually entering their credentials.\nNon-interactive events can occur frequently. Entra may group similar events together in the sign-in logs to make them easier to review.\nJust like lastSignInDateTime, this property records an attempt, so it may represent either a successful or unsuccessful authentication attempt.\n3. Last successful sign-in # The most useful property for many inactive-account scenarios is:\nlastSuccessfulSignInDateTime Unlike the previous two properties, this value only moves forward when authentication actually succeeds.\nIt includes both:\nSuccessful interactive authentication OR Successful non-interactive authentication Microsoft describes this property as the most recent successful interactive or non-interactive sign-in and specifically recommends it for determining when an account was genuinely accessed.\nFor an inactive-account lifecycle process, this is normally the strongest starting point.\nFor example:\nCurrent date: 1 March Last interactive attempt: 20 February Last successful sign-in: 15 November If you used lastSignInDateTime, the account might appear to have been active only nine days ago.\nBut that February authentication could have failed.\nUsing lastSuccessfulSignInDateTime shows that the account has not successfully authenticated since November.\nThat is a very different result.\nWhy the Entra portal and an export may show different dates # This difference can cause confusion when someone compares an automated report against the Entra admin centre.\nImagine an automated report contains:\nUser: Alex Smith Last successful sign-in: 24 November 2025 Someone opens the user\u0026rsquo;s Entra profile and sees:\nLast interactive sign-in: 2 February 2026 It is tempting to assume the export is outdated.\nIt may not be.\nThe values are measuring two different things:\n24 November Successful authentication ↓ lastSuccessfulSignInDateTime = 24 November 2 February Interactive authentication attempt ↓ Authentication fails ↓ lastSignInDateTime = 2 February ↓ lastSuccessfulSignInDateTime remains 24 November The next step would be to examine the user\u0026rsquo;s detailed sign-in logs around 2 February and check the Status of the event.\nWhich date should I use for inactive accounts? # For most inactive-account reporting, I would use:\nlastSuccessfulSignInDateTime as the primary activity date.\nThe logic becomes:\nHas the account successfully authenticated recently? ↓ YES ↓ Account active OR Has the account successfully authenticated recently? ↓ NO ↓ Potentially inactive ↓ Apply additional checks Those additional checks are important.\nA sign-in timestamp alone should normally not be enough to automatically delete an identity.\nDepending on the environment, you may also need to consider things such as:\naccount creation date whether the account is enabled account type service accounts shared or resource accounts meeting-room accounts guest users approved exemptions administrative accounts assigned licences business ownership The inactivity date identifies candidates. Your organisation\u0026rsquo;s lifecycle rules determine what happens next.\nAn important limitation: 1 December 2023 # There is an important caveat when using:\nlastSuccessfulSignInDateTime Microsoft made this property available from 1 December 2023, and historical successful sign-ins were not backfilled.\nThis means a value such as:\n\u0026#34;lastSuccessfulSignInDateTime\u0026#34;: null does not always mean:\nThis user has never signed in.\nFor an old account, it could instead mean there has been no successful sign-in recorded for this property since it became available.\nTherefore, avoid treating null as automatic evidence that an account is unused.\nFor older accounts, you may need additional evidence such as the account creation date, available sign-in information and your organisation\u0026rsquo;s lifecycle policy.\nRetrieving sign-in activity with Microsoft Graph # A simple Graph request can retrieve the relevant properties:\nGET https://graph.microsoft.com/v1.0/users?$select=id,displayName,userPrincipalName,accountEnabled,createdDateTime,signInActivity A returned user might look similar to this:\n{ \u0026#34;id\u0026#34;: \u0026#34;00000000-0000-0000-0000-000000000000\u0026#34;, \u0026#34;displayName\u0026#34;: \u0026#34;Alex Smith\u0026#34;, \u0026#34;userPrincipalName\u0026#34;: \u0026#34;alex.smith@contoso.com\u0026#34;, \u0026#34;accountEnabled\u0026#34;: true, \u0026#34;createdDateTime\u0026#34;: \u0026#34;2024-01-08T09:22:14Z\u0026#34;, \u0026#34;signInActivity\u0026#34;: { \u0026#34;lastSignInDateTime\u0026#34;: \u0026#34;2026-02-02T10:12:31Z\u0026#34;, \u0026#34;lastNonInteractiveSignInDateTime\u0026#34;: \u0026#34;2026-01-14T08:43:11Z\u0026#34;, \u0026#34;lastSuccessfulSignInDateTime\u0026#34;: \u0026#34;2025-11-24T14:06:52Z\u0026#34; } } From this result we can determine:\nLatest interactive attempt: 2 February 2026 Latest non-interactive attempt: 14 January 2026 Latest successful authentication: 24 November 2025 For an inactivity calculation, I would normally base the calculation on:\n24 November 2025 rather than the February interactive attempt.\nUsing the data in Power Automate # The same Graph request can be called from Power Automate.\nFor example, the successful timestamp for the current user could be referenced with an expression similar to:\nitems(\u0026#39;Apply_to_each\u0026#39;)?[\u0026#39;signInActivity\u0026#39;]?[\u0026#39;lastSuccessfulSignInDateTime\u0026#39;] Before performing date calculations, check whether the value exists:\nempty( items(\u0026#39;Apply_to_each\u0026#39;)?[\u0026#39;signInActivity\u0026#39;]?[\u0026#39;lastSuccessfulSignInDateTime\u0026#39;] ) This allows the flow to handle users with no available successful sign-in timestamp separately rather than treating a null value as a valid date.\nA simplified lifecycle might look like:\nGet users from Microsoft Graph ↓ Exclude account types outside the process ↓ Read lastSuccessfulSignInDateTime ↓ Is a value available? ↓ ↓ YES NO ↓ ↓ Calculate Apply separate inactivity fallback logic ↓ Compare against inactivity threshold ↓ Warning / review / remediation Do not forget Graph pagination # There is another important consideration when retrieving users at scale.\nWhen signInActivity is included in a users query, Microsoft Graph limits the maximum page size to 500 users.\nFor a tenant containing thousands of accounts, one request will therefore not return everybody.\nYour automation needs to follow:\n@odata.nextLink until no next link is returned.\nOtherwise, an inactive-account report might silently analyse only the first page of users.\nPermissions and licensing # Microsoft currently documents signInActivity as requiring Microsoft Entra ID P1 or P2 and the AuditLog.Read.All permission.\nWhen building an application-based automation that retrieves users and their activity, commonly required Microsoft Graph application permissions include:\nUser.Read.All AuditLog.Read.All As with any Graph integration, grant only the permissions required by the solution.\nSign-in activity is not real-time # Another source of apparent discrepancies is replication delay.\nThe summary signInActivity properties are not designed as real-time authentication telemetry and Microsoft notes that they can take up to 24 hours to update.\nTherefore, if somebody has just signed in and you need to confirm it immediately, use the detailed Entra sign-in logs instead.\nThink of the two sources differently:\nSource Best used for signInActivity Reporting, lifecycle automation and identifying potentially inactive accounts Sign-in logs Detailed investigation and recent authentication events Investigating a discrepancy # If an automated report and Entra appear to disagree, I normally investigate the individual account before assuming either value is wrong.\nStart in:\nMicrosoft Entra admin centre → Entra ID → Monitoring \u0026amp; health → Sign-in logs Filter the results to the affected user and examine events around the date in question.\nCheck:\nDate and time Status Application Resource Interactive / non-interactive Conditional Access Failure reason If the newer interactive event shows a failure, the difference is explained:\nLast interactive sign-in ≠ Last successful sign-in For programmatic investigations, individual sign-in events can also be queried through:\nGET https://graph.microsoft.com/v1.0/auditLogs/signIns Unlike signInActivity, this endpoint returns individual authentication events and can therefore be used to investigate exactly what happened during a particular sign-in.\nA useful rule of thumb # I find the easiest way to remember the difference is:\nlastSignInDateTime = When did the user last TRY to interactively authenticate? lastNonInteractiveSignInDateTime = When did something last TRY to authenticate on behalf of the user? lastSuccessfulSignInDateTime = When was the account last ACTUALLY authenticated successfully? For security investigations, all three can be useful.\nFor determining genuine account usage, the successful timestamp is usually the most meaningful.\nFinal thoughts # Sign-in dates look simple until they become part of an automated lifecycle process.\nThe key lesson is that a sign-in attempt is not the same as a successful sign-in.\nUsing lastSignInDateTime alone can make an inactive account appear active because even an unsuccessful login attempt can update the value.\nFor inactive-account reporting, lastSuccessfulSignInDateTime generally gives a much better indication of genuine account usage, provided that its December 2023 historical limitation and possible reporting latency are handled correctly.\nOnce those differences are understood, Entra sign-in data becomes much easier to interpret — and much safer to use as the basis for automated identity lifecycle decisions.\nMicrosoft documentation used # Microsoft Graph — signInActivity resource type Microsoft Graph — List users and retrieve signInActivity Microsoft Entra — Sign-in log activity details Microsoft Entra — Log latency and last sign-in information Microsoft Graph — List sign-in events ","date":"22 September 2026","externalUrl":null,"permalink":"/guides/understanding-entra-sigin-dates/","section":"Guides","summary":"","title":"Understanding Entra Sign-In Dates","type":"guides"},{"content":"Power Automate expressions are incredibly useful. A quick if(), concat() or formatDateTime() can often save you from adding several extra actions to a flow.\nBut there comes a point where trying to do everything with expressions makes a flow difficult to read, difficult to troubleshoot and even harder to maintain.\nIf most of the work is happening inside an Excel workbook, that is often the point where Office Scripts become a better option.\nThis guide explains how to decide between the two.\nThe Simple Rule # A useful rule of thumb is:\nUse Power Automate expressions for small pieces of workflow logic. Use Office Scripts for substantial Excel manipulation.\nThink of Power Automate as the orchestrator.\nIt decides:\nwhen something should happen; which file should be processed; whether a condition has been met; who should receive an email; where the result should be saved. Office Scripts can act as the Excel worker.\nThey can:\nread ranges; process many rows; remove or create columns; add formulas; format worksheets; create tables; manipulate large sections of a workbook; return processed information back to Power Automate. Microsoft supports running Office Scripts directly from Power Automate and passing parameters into a script or returning results from it.\nExpressions are ideal when the operation is small, easy to understand and relates to the flow rather than the workbook.\nExample 1 - Checking a Value # Suppose you want to determine whether an account is an admin account based on its UPN.\nAn expression such as:\nif( endsWith( items(\u0026#39;Apply_to_each\u0026#39;)?[\u0026#39;userPrincipalName\u0026#39;], \u0026#39;@contoso.onmicrosoft.com\u0026#39; ), \u0026#39;Admin\u0026#39;, \u0026#39;Standard\u0026#39; ) is perfectly reasonable.\nCreating an Office Script just to perform this check would add unnecessary complexity.\nExample 2 - Formatting a Date # If you receive:\n2026-09-22T10:34:15Z and simply want:\n22/09/2026 a Power Automate expression is enough:\nformatDateTime( triggerBody()?[\u0026#39;createdDateTime\u0026#39;], \u0026#39;dd/MM/yyyy\u0026#39; ) Again, there is little benefit in introducing a script.\nExample 3 - Creating a String # If you need to combine a user\u0026rsquo;s name and email:\nconcat( item()?[\u0026#39;displayName\u0026#39;], \u0026#39; - \u0026#39;, item()?[\u0026#39;mail\u0026#39;] ) this is exactly the type of small transformation Power Automate expressions handle well.\nWhen Office Scripts Start to Make More Sense # Office Scripts become particularly useful when the automation involves performing several operations against Excel data.\nMicrosoft describes Office Scripts as a way of recording or coding repeatable Excel operations and then running those operations manually or through Power Automate.\nHere are some common signs that a script may be the cleaner solution.\n1. You Are Processing Lots of Excel Rows # Imagine an Excel workbook containing several thousand rows.\nYou need to:\nread every row; check several columns; calculate a value; update another column; repeat the process for every row. You could build something like:\nList rows present in a table ↓ Apply to each ↓ Condition ↓ Compose ↓ Condition ↓ Update a row But this quickly creates a large number of Power Automate actions.\nInstead, Power Automate could simply run:\nRun script and let the script process the workbook internally.\n2. You Need to Manipulate the Workbook Itself # Office Scripts are particularly useful when your requirement sounds like something a person would normally do manually in Excel.\nFor example:\nOpen the workbook, remove columns C and F, rename column A, create a table, format the headers and autofit the columns.\nTrying to reproduce all of that through Power Automate is normally unnecessary.\nAn Office Script is much better suited to it.\nFor example:\nfunction main(workbook: ExcelScript.Workbook) { const sheet = workbook.getWorksheet(\u0026#34;Report\u0026#34;); const usedRange = sheet.getUsedRange(); usedRange.getFormat().autofitColumns(); usedRange.getFormat().autofitRows(); } Power Automate can simply locate the workbook and run the script.\nExample - Cleaning an Imported Report # Consider a daily CSV or Excel export.\nEvery morning you need to:\nremove unnecessary columns; rename some headings; remove blank rows; add calculated columns; create an Excel table; apply formatting; resize the columns. Without Office Scripts, your flow could become quite complicated.\nWith Office Scripts, the flow could instead look something like:\nScheduled Flow │ ▼ Download Report │ ▼ Save File to SharePoint │ ▼ Run Office Script │ ▼ Continue Processing The script contains the Excel-specific logic.\nThe flow controls the overall process.\nMicrosoft uses a very similar scenario when explaining Office Scripts: repeatedly cleaning and formatting imported data is a strong use case for scripting.\nAnother Good Example - Transforming a Table # Suppose Excel contains:\nUser Licence Monthly Cost Alex E5 40 Sam Power BI 10 John E5 40 You want to:\ninspect every row; ignore certain licences; calculate totals; add a calculated column; return a summary. You could potentially achieve this using:\nApply to each; Filter array; Select; variables; Compose actions; expressions. For a small dataset, that may be fine.\nBut if the logic continues growing, moving the calculation into a script can make the automation much easier to understand.\nThe flow then becomes:\nGet workbook ↓ Run script ↓ Receive result ↓ Send report Office Scripts can return values and objects back to Power Automate, where the result becomes available as dynamic content.\nA Useful Warning Sign: The Giant Expression # You may have seen expressions that start reasonably:\nif(...) Then become:\nif( and( or( equals(...), not(empty(...)) ), greater(...) ), if(...), if(...) ) Eventually nobody wants to touch them.\nComplex expressions aren\u0026rsquo;t automatically bad, but if you find yourself repeatedly nesting:\nif() and() or() contains() replace() split() select() it is worth asking:\nWould this logic be easier to understand as normal code?\nIf the answer is yes — and the processing is closely related to Excel — Office Scripts may be the cleaner option.\nWhat About Apply to Each? # Apply to each isn\u0026rsquo;t bad either.\nFor example:\nGet users ↓ Apply to each user ↓ Send email is completely normal.\nBut consider:\nGet 5,000 Excel rows ↓ Apply to each row ↓ Condition ↓ Compose ↓ Condition ↓ Update Excel row Now Power Automate is repeatedly communicating with Excel thousands of times.\nIf the task is primarily about manipulating the workbook, it may be cleaner to let an Office Script work with the range directly.\nWhen I Would NOT Use Office Scripts # Office Scripts aren\u0026rsquo;t automatically better just because they involve code.\nI wouldn\u0026rsquo;t introduce one simply to replace:\nconcat() or:\nformatDateTime() or:\nif(equals(...)) I would also avoid using a script if the process has very little to do with Excel.\nFor example:\nMicrosoft Graph ↓ Check account type ↓ Send email ↓ Update SharePoint There is little reason to introduce Office Scripts here.\nPower Automate expressions are a natural fit.\nThe Hybrid Approach # In many real-world automations, the best solution isn\u0026rsquo;t:\nPower Automate OR Office Scripts\nIt is:\nPower Automate + Office Scripts\nFor example:\nPower Automate │ ├── Trigger every morning │ ├── Download report │ ├── Save workbook │ ├── Run Office Script │ │ │ ├── Clean workbook │ ├── Process rows │ ├── Add calculations │ └── Return summary │ ├── Check returned result │ ├── Save processed file │ └── Send notification Each technology is doing what it is good at.\nQuick Decision Guide # Requirement Better Choice Format a date Power Automate expression Check whether a value is empty Power Automate expression Build a filename Power Automate expression Check an email domain Power Automate expression Perform a simple calculation Power Automate expression Remove multiple Excel columns Office Script Format an entire workbook Office Script Process thousands of Excel rows Consider Office Script Create formulas across many rows Office Script Create or modify worksheets Office Script Create Excel tables Office Script Clean a recurring Excel export Office Script Send emails or Teams notifications Power Automate Call Microsoft Graph Power Automate Update SharePoint Power Automate Orchestrate the entire process Power Automate A Simple Test # When deciding between them, I normally ask three questions.\nIs this just one small transformation? # Use a Power Automate expression.\nAm I performing several operations against an Excel workbook? # Consider an Office Script.\nDoes the automation involve Excel plus other Microsoft 365 services? # Use Power Automate to orchestrate the process and Office Scripts to handle the Excel work.\nThat separation usually results in flows that are easier to read, troubleshoot and maintain.\nOne Thing to Keep in Mind # Office Scripts running through Power Automate aren\u0026rsquo;t unlimited.\nFor example, Microsoft currently documents a 120-second timeout for synchronous Power Automate script operations, along with limits on script calls and the amount of Excel data transferred.\nSo Office Scripts shouldn\u0026rsquo;t simply become a place to move every complicated piece of logic.\nFor very large datasets or long-running data processing, another technology may be more appropriate.\nFinal Takeaway # Don\u0026rsquo;t move to Office Scripts just because a Power Automate expression looks technical.\nMove to Office Scripts when Excel itself has become the thing you are automating.\nA good automation often follows this pattern:\nPower Automate decides what needs to happen. Office Scripts handle what needs to happen inside Excel.\nKeeping that separation in mind can make even fairly complex Excel automations considerably easier to build and support.\n","date":"22 September 2026","externalUrl":null,"permalink":"/guides/when-use-office-scripts-instead-pa/","section":"Guides","summary":"","title":"When to Use Office Scripts Instead of Power Automate Expressions","type":"guides"},{"content":"I\u0026rsquo;m a Power Platform and Microsoft 365 engineer focused on building practical solutions that improve processes, reduce manual administration and make technology easier for people to use.\nMy work combines low-code development with Microsoft 365, cloud services, APIs, automation and governance.\nThis portfolio documents selected projects and technical lessons from that work.\n","date":"21 September 2026","externalUrl":null,"permalink":"/about/","section":"Home","summary":"","title":"About","type":"page"},{"content":" The challenge # Large Microsoft 365 environments can accumulate user accounts that remain enabled long after they stop being actively used.\nManually identifying these accounts is difficult because inactivity cannot be determined reliably from a single value. Administrators need to consider sign-in activity, account type, exemptions, exceptional cases and whether the user has subsequently become active again.\nThe objective was to create a controlled lifecycle process that could identify genuinely inactive accounts while avoiding inappropriate or premature removal.\nThe solution # I designed an automated lifecycle process using Power Automate and Microsoft Graph.\nThe solution retrieves user and sign-in information, evaluates each account against defined inactivity rules and tracks eligible users through a structured lifecycle.\nRather than immediately disabling or deleting an account, the process progresses users through warning stages before making them eligible for removal.\nA separate lifecycle record provides a persistent history of warnings, exceptions, reactivation and eventual deletion.\nArchitecture # The high-level process is:\nMicrosoft Entra ID → Microsoft Graph → Power Automate → Eligibility evaluation → Lifecycle tracking → Warning notifications → Final eligibility check → Account removal\nA SharePoint list is used to maintain the state of each lifecycle record independently from the directory itself.\nThis allows the automation to determine whether an account is:\nnewly identified as inactive currently within a warning period exempt from automated processing reactivated eligible for deletion already processed Safeguards and governance # Account deletion is deliberately separated from initial inactivity detection.\nThe solution includes controls such as:\nexclusion of non-user and service accounts configurable exemption groups staged warning notifications repeated sign-in checks before destructive actions exception handling for accounts that require manual review persistent lifecycle history separate deletion eligibility and deletion stages detection of users who become active again This creates a controlled process rather than treating inactivity as a simple delete condition.\nAdditional operational context # The process can also enrich the lifecycle record with information useful to other operational teams.\nFor example, associated licence information can help quantify the cost of inactive accounts, while device information can identify cases where equipment may need to be recovered if an account is removed.\nThis turns the solution from a simple account-cleanup process into a wider identity lifecycle and operational-governance workflow.\nOutcome # The solution reduces the administrative effort involved in repeatedly identifying and reviewing inactive accounts while introducing a consistent and auditable lifecycle.\nAccounts are evaluated using repeatable rules, users receive appropriate warning periods and administrators retain mechanisms for exemptions and manual intervention.\nThe design also separates detection, notification and destructive operations, reducing the risk associated with fully automated account removal.\nTechnologies # Microsoft Entra ID · Microsoft Graph · Power Automate · SharePoint · Microsoft 365\n","date":"21 September 2026","externalUrl":null,"permalink":"/projects/inactive-account-lifecycle/","section":"Projects","summary":"Automating inactive-account governance using Microsoft Graph, Power Automate and structured lifecycle tracking.","title":"Automated Inactive Account Lifecycle Management","type":"projects"},{"content":"","date":"21 September 2026","externalUrl":null,"permalink":"/tags/azure-automation/","section":"Tags","summary":"","title":"Azure Automation","type":"tags"},{"content":"","date":"21 September 2026","externalUrl":null,"permalink":"/tags/dataverse/","section":"Tags","summary":"","title":"Dataverse","type":"tags"},{"content":"Adaptive Cards are a useful way of turning Power Automate notifications into structured Microsoft Teams messages.\nThey work particularly well for things such as:\napplication credential expiry alerts; service health notifications; approval summaries; licence reports; security alerts; operational dashboards. However, there is an easy problem to encounter when a card is generated dynamically:\nThe payload is too large. Please make sure the size is less than 28KB. or:\nRequest Entity too large The flow itself may be working correctly. The problem is simply that the message being sent to Teams has grown beyond the size supported by the connector.\nThis guide explains how I troubleshoot these errors and, more importantly, how I design Adaptive Card notifications so they do not become increasingly fragile as the amount of data grows.\nThe problem # Consider a Power Automate flow that checks Azure application credentials which are approaching expiration.\nThe flow might:\nGet applications ↓ Check credential expiry dates ↓ Filter credentials expiring soon ↓ Build Adaptive Card rows ↓ Post card to Teams With only a few applications, everything works correctly.\nAs more results are returned, however, the Adaptive Card becomes larger.\nEventually the Teams action fails with:\nThe payload is too large. Please make sure the size is less than 28KB. Microsoft documents an approximate 28 KB message-size limit for relevant Microsoft Teams connector posting actions.\nThe important word here is payload.\nIt is not simply the amount of text visible to the user.\nThe JSON required to construct the card contributes to the message as well.\nWhy an Adaptive Card becomes large so quickly # A card containing ten short rows may look tiny when rendered in Teams.\nThe JSON behind it can be considerably larger.\nFor example, a single visual row might look something like this:\n{ \u0026#34;type\u0026#34;: \u0026#34;ColumnSet\u0026#34;, \u0026#34;columns\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;Column\u0026#34;, \u0026#34;width\u0026#34;: \u0026#34;stretch\u0026#34;, \u0026#34;items\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;TextBlock\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;Example Application\u0026#34;, \u0026#34;wrap\u0026#34;: true } ] }, { \u0026#34;type\u0026#34;: \u0026#34;Column\u0026#34;, \u0026#34;width\u0026#34;: \u0026#34;auto\u0026#34;, \u0026#34;items\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;TextBlock\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;18 days\u0026#34;, \u0026#34;wrap\u0026#34;: true, \u0026#34;weight\u0026#34;: \u0026#34;Bolder\u0026#34; } ] } ] } What the user sees is effectively:\nExample Application 18 days But Power Automate has to send the entire JSON structure.\nNow repeat that structure 20, 30 or 50 times.\nThis is why cards containing repeated ColumnSet, Container, TextBlock and formatting properties can reach the payload limit surprisingly quickly.\nFirst: confirm that size is actually the problem # Before redesigning the flow, I check the failed Teams action.\nOpen:\nFlow → Run history → Failed run → Post card in a chat or channel Then inspect the error.\nIf it contains something similar to:\nThe payload is too large or:\nRequest Entity too large the issue is different from problems such as:\nmalformed JSON; unsupported Adaptive Card schema versions; invalid image URLs; missing properties; authentication failures; Teams connector throttling. Microsoft also recommends validating Adaptive Card JSON when troubleshooting card failures, as malformed JSON is another common source of errors.\nInspect the generated card before posting it # If the Adaptive Card is generated dynamically, I usually add a Compose action immediately before the Teams action.\nFor example:\nCompose – Final Adaptive Card Its input contains exactly what will be passed into:\nPost card in a chat or channel This gives me a much easier way of inspecting the completed JSON.\nThe flow becomes:\nBuild card ↓ Compose – Final Adaptive Card ↓ Post card in Teams I can then open the flow run and inspect the output of the Compose action.\nThis is particularly useful when the card is assembled using several variables, Select actions or Append to array variable actions.\nUsing length() as an early warning # Power Automate\u0026rsquo;s length() expression can give a useful indication of whether the card is becoming unusually large.\nFor example:\nlength( string(outputs(\u0026#39;Compose_-_Final_Adaptive_Card\u0026#39;)) ) This returns the number of characters in the generated string.\nIt is important to understand that:\ncharacters ≠ bytes Characters containing Unicode data can require more than one byte when encoded.\nTherefore I would not use this expression as an exact measurement of the Teams limit.\nIt is still useful as a troubleshooting metric.\nFor example, I might temporarily add:\nCompose – Card Character Count with:\nlength( string(outputs(\u0026#39;Compose_-_Final_Adaptive_Card\u0026#39;)) ) If the number increases dramatically with the number of returned records, I have a strong indication of where the problem is coming from.\nFix 1 - Do not put the complete report in Teams # This is usually the most important change.\nAn Adaptive Card should generally be treated as a notification or summary, not as a replacement for a reporting system.\nInstead of displaying:\n47 applications 47 application IDs 47 expiry dates 47 owners 47 credential types 47 links I might display:\nApplication Credential Expiry Alert 3 credentials expire within 7 days 8 credentials expire within 30 days 36 additional credentials require review Highest priority: Application A 2 days Application B 4 days Application C 6 days Application D 8 days Application E 10 days [View full report] The complete dataset could live in:\nSharePoint; Dataverse; Power BI; an internal web application; another reporting location. The card then becomes an entry point into the report rather than the report itself.\nThis pattern scales much better.\nFix 2 - Limit the number of records displayed # Power Automate\u0026rsquo;s take() expression is particularly useful here.\nSuppose an array contains all applications approaching expiration:\nvariables(\u0026#39;varExpiringCredentials\u0026#39;) Instead of placing every result in the card:\nvariables(\u0026#39;varExpiringCredentials\u0026#39;) I can use:\ntake( variables(\u0026#39;varExpiringCredentials\u0026#39;), 10 ) The card will display only the first ten results.\nI can then show the total separately:\nlength( variables(\u0026#39;varExpiringCredentials\u0026#39;) ) This allows the notification to say something like:\n24 credentials require attention. Showing the 10 most urgent. This is considerably more useful than allowing the entire notification to fail because there were too many results.\nSort before using take() # If I am going to limit the card to ten results, I also want to make sure they are the most important ten.\nFor an expiry notification, that normally means sorting the dataset so that credentials expiring soonest appear first.\nConceptually:\nAll expiring credentials ↓ Sort by DaysRemaining ↓ Take first 10 ↓ Build Adaptive Card The Teams notification therefore contains the records requiring the most immediate attention.\nThe full dataset can still be stored elsewhere.\nFix 3 - Simplify repeated card JSON # Sometimes the underlying problem is not the amount of data but the way each row is constructed.\nConsider a card containing heavily formatted rows with:\nContainer └─ ColumnSet ├─ Column │ └─ TextBlock ├─ Column │ └─ TextBlock └─ Column └─ TextBlock If this structure is repeated 30 times, a large proportion of the payload consists of formatting instructions rather than useful data.\nWhere possible, I remove unnecessary properties such as repeated:\n\u0026#34;spacing\u0026#34;: \u0026#34;None\u0026#34; \u0026#34;wrap\u0026#34;: true \u0026#34;horizontalAlignment\u0026#34;: \u0026#34;Left\u0026#34; or unnecessary nested containers.\nI do not remove properties that are required for the desired layout, but it is worth reviewing whether every generated row really needs several nested Adaptive Card objects.\nFix 4 - Combine values # Another useful technique is combining related fields.\nFor example, instead of displaying:\nApplication Name | Application ID | Expiry as three separate columns, I could display:\nApplication Name Application ID inside one element and keep only the expiry period as the second column.\nEven better, where appropriate, the application name could link directly to the management page.\nThis gives users the same useful information while reducing the number of card components.\nFix 5 - Remove data users do not need # An operational notification can easily become overloaded.\nFor example, the automation may know:\nApplication name Object ID Application ID Credential ID Credential type Created date Expiry date Days remaining Owner Owner UPN Owner object ID Environment Status That does not mean every field needs to appear in Teams.\nI normally ask:\nWhat information does someone actually need to decide whether they need to act?\nFor an expiry notification that may only be:\nApplication Credential type Expiry date Days remaining Owner Everything else can remain in the underlying report.\nFix 6 - Be careful with long URLs # URLs are also part of the card payload.\nA card containing many long links can therefore become surprisingly large.\nInstead of displaying URLs as text:\nhttps://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/... use a short label such as:\nView application with the URL stored in the Adaptive Card action.\nThis improves both readability and card size.\nIt does not eliminate the URL from the payload, but it prevents the same long URL from also being presented as unnecessary visible text.\nFix 7 - Split results across multiple cards # If users genuinely need every record directly in Teams, another option is to split the data into smaller batches.\nFor example:\nResults: 36 Card 1 → records 1–10 Card 2 → records 11–20 Card 3 → records 21–30 Card 4 → records 31–36 Power Automate provides useful expressions for this:\ntake() and:\nskip() For example:\ntake( skip( variables(\u0026#39;varResults\u0026#39;), variables(\u0026#39;varOffset\u0026#39;) ), variables(\u0026#39;varBatchSize\u0026#39;) ) With:\nvarOffset = 0 varBatchSize = 10 the first iteration returns:\n0–9 After increasing the offset to:\n10 the next iteration returns:\n10–19 and so on.\nBut splitting cards should not be the first solution # Technically, sending six cards instead of one may solve the payload-size problem.\nFrom a user experience perspective, however, this:\nAlert Alert Alert Alert Alert Alert may not be particularly useful.\nIt also increases the number of calls being made to the Teams connector and can introduce additional considerations around throttling.\nI therefore prefer:\nOne concise Teams notification + Full external report unless there is a genuine requirement for all records to be displayed directly in Teams.\nFix 8 - Add a maximum number of displayed records # I like to build this protection directly into flows rather than waiting for a payload error.\nFor example:\nvarMaxCardItems = 10 Then:\ntake( variables(\u0026#39;varResults\u0026#39;), variables(\u0026#39;varMaxCardItems\u0026#39;) ) This means the size of the card remains predictable even if the underlying dataset grows from:\n5 records to:\n500 records The notification does not become progressively larger.\nShow how many results were hidden # If I limit the results, I also make that clear to the user.\nFor example:\nlength(variables(\u0026#39;varResults\u0026#39;)) could provide the total count.\nI could then add a message such as:\n24 credentials require attention. The 10 most urgent are shown below. Or calculate the number not displayed:\nsub( length(variables(\u0026#39;varResults\u0026#39;)), variables(\u0026#39;varMaxCardItems\u0026#39;) ) Producing:\n14 additional credentials are included in the full report. This makes it clear that the card represents a summary rather than the complete result set.\nA better architecture # Instead of:\nRetrieve results ↓ Create enormous Adaptive Card ↓ Post everything to Teams I prefer:\nRetrieve results ↓ Process and classify ↓ Store complete results ↓ Sort by priority ↓ Take highest-priority records ↓ Generate summary ↓ Post small Adaptive Card ↓ Link to complete results This design has several advantages.\nThe Teams card remains:\nreadable; predictable; fast; less likely to exceed connector limits; useful even if the dataset grows significantly. Example card design # Instead of trying to display every expiring credential, I might design the card like this:\nAPPLICATION CREDENTIAL EXPIRY 27 credentials require attention. CRITICAL 3 expire within 7 days WARNING 8 expire within 30 days NEXT TO EXPIRE Finance Reporting API 2 days HR Integration 4 days Service Management App 5 days Document Processing 9 days Automation Service 12 days Showing the 5 most urgent of 27. [View Full Report] This provides the important information immediately without attempting to reproduce an entire reporting table inside Teams.\nAdd severity summaries instead of more rows # Aggregated values often communicate more useful information than dozens of records.\nFor example:\n≤ 7 days 3 8–30 days 8 31–60 days 11 61–90 days 5 This gives someone an immediate indication of risk.\nThe detailed records can then be shown only for the highest-priority category.\nWhy reducing the date range may not solve it # A common troubleshooting step is changing something such as:\nCredentials expiring within 90 days to:\nCredentials expiring within 30 days This may help, but it does not address the underlying design issue.\nIf the dataset grows again, the flow can fail again.\nThe same problem occurs if the fix is simply:\nRemove a few rows The flow may work today and fail next month.\nI therefore prefer putting an explicit maximum size on the number of records presented, rather than relying on the dataset naturally remaining small.\nDon\u0026rsquo;t confuse payload limits with throttling # There are two different problems that can look similar during troubleshooting.\nPayload problem # One message is too large. Typical error:\nRequest Entity too large The solution is usually:\nMake each message smaller. Throttling problem # Too many requests are being made in a short period. The solution normally involves things such as:\nreducing request frequency; controlling concurrency; introducing delays where appropriate; reviewing connector limits. Splitting one oversized card into 20 smaller cards may fix the first problem while increasing the chances of encountering the second.\nThat is another reason why I prefer concise summary cards where possible.\nValidate the finished Adaptive Card # Payload size is not the only reason an Adaptive Card can fail.\nBefore assuming every error is related to size, I also check:\nJSON syntax; quotation marks; commas and braces; supported schema properties; dynamic values; image URLs; Adaptive Card version compatibility. Microsoft recommends validating generated card JSON when troubleshooting Adaptive Cards used with Power Automate.\nThis is especially useful when card JSON has been dynamically assembled rather than written as one static object.\nAdd defensive logic # For important production automation, I prefer the flow to protect itself.\nFor example:\nRetrieve results ↓ Count results ↓ Are there more than 10? ↓ Yes No ↓ ↓ Top 10 All └────┬────┘ ↓ Build Adaptive Card ↓ Post to Teams The flow therefore controls card growth deliberately.\nAnother option is always using:\ntake(array, 10) regardless of the number of results.\nIf fewer than ten records exist, take() simply returns those available.\nWhat I would avoid # Increasing the card until it barely fits # If a card currently sits just below the connector limit, it has very little tolerance for future growth.\nI would rather redesign it than optimise it to sit immediately below the maximum.\nHard-coding assumptions about data volume # For example:\nThere will never be more than 20 applications. Systems change.\nA good notification design should remain reliable even when the amount of underlying data increases substantially.\nTreating Teams as the database # Teams is an excellent place to tell someone:\nSomething requires your attention. It is not necessarily the best place to present hundreds of rows of operational data.\nSending dozens of cards by default # Chunking is useful when there is a genuine requirement for detailed information inside Teams.\nIt should not normally be the first solution to a reporting problem.\nRecommended pattern # For most operational alerts, my preferred pattern is:\nFULL DATASET │ ▼ SharePoint / Dataverse │ │ ┌───────┴────────┐ │ │ ▼ ▼ Power BI Power Automate │ ▼ Sort by urgency │ ▼ Take top N │ ▼ Create summary │ ▼ Teams Adaptive Card │ ▼ View Full Report Teams provides the notification.\nThe underlying system provides the detail.\nTroubleshooting checklist # When I encounter an Adaptive Card payload error, I normally work through the following checks:\nConfirm the actual Teams action error.\nInspect the completed Adaptive Card JSON.\nCheck how many dynamic records are being inserted.\nLook for deeply repeated Container, ColumnSet and TextBlock structures.\nRemove unnecessary fields and formatting.\nSort the results by importance.\nUse take() to cap the number shown.\nDisplay the total number of results separately.\nMove the complete dataset to a proper reporting location.\nAdd a link from Teams to the complete report.\nConsider chunking only where users genuinely need every record in Teams.\nTest the flow with a deliberately large dataset rather than only the normal development dataset.\nTesting for scale # One lesson I find particularly important is testing the automation with more data than I expect it to encounter normally.\nA card that works with:\n3 records does not prove that it will work with:\n30 records When developing a dynamic Adaptive Card, I therefore try to test:\nSmall dataset ↓ Normal dataset ↓ Large dataset ↓ Worst reasonable case This helps identify payload-size problems before they occur in production.\nFinal design principle # The most important lesson is that an Adaptive Card should not grow indefinitely with the underlying dataset.\nInstead of thinking:\nHow can I squeeze all these records into the card? I prefer to ask:\nWhat does the person receiving this notification actually need to know? Usually the answer is:\nWhat happened? How serious is it? What requires attention first? How many items are affected? Where can I see the details? A card designed around those questions will usually be smaller, easier to understand and considerably more resilient.\nKey takeaway # If a Power Automate Teams action fails because an Adaptive Card exceeds the payload limit, reducing a few rows may get the flow running again, but it does not solve the underlying scalability problem.\nA stronger pattern is:\nSummarise + Prioritise + Limit + Link to the detail That turns the Adaptive Card from an oversized report into what it is particularly good at being:\na concise, actionable notification.\n","date":"21 September 2026","externalUrl":null,"permalink":"/guides/fixing-adaptive-card-payload-limit/","section":"Guides","summary":"","title":"Fixing Teams Adaptive Card Payload Limit Errors in Power Automate","type":"guides"},{"content":"When working with Microsoft Graph in Power Automate, one of the easiest mistakes to make is assuming that a successful API call has returned all available records.\nIn many cases, it has not.\nMicrosoft Graph frequently returns data in pages. If there are more records available than can be returned in a single response, Graph provides an @odata.nextLink property containing the URL for the next page.\nIf that link is ignored, a flow may process only the first set of results without producing an obvious error.\nThis guide shows how I handle Microsoft Graph pagination in Power Automate so that a flow continues retrieving records until every page has been processed.\nThe problem # Consider a request such as:\nGET https://graph.microsoft.com/v1.0/users A simplified response may look like this:\n{ \u0026#34;@odata.context\u0026#34;: \u0026#34;https://graph.microsoft.com/v1.0/$metadata#users\u0026#34;, \u0026#34;value\u0026#34;: [ { \u0026#34;id\u0026#34;: \u0026#34;12345\u0026#34;, \u0026#34;displayName\u0026#34;: \u0026#34;Example User\u0026#34; } ], \u0026#34;@odata.nextLink\u0026#34;: \u0026#34;https://graph.microsoft.com/v1.0/users?$skiptoken=...\u0026#34; } The important property is:\n@odata.nextLink Its presence means:\nThere are more records available.\nThe URL already contains the information Graph needs to retrieve the next page, normally including a paging token.\nThe flow should therefore:\nMake the initial Graph request. Process or store the returned value array. Check for @odata.nextLink. If it exists, call the URL provided by Graph. Repeat until no next link is returned. Why this matters # Pagination can easily go unnoticed during development.\nFor example, a flow might be tested in a development tenant containing only a few dozen accounts. The same flow could later run against a production tenant containing thousands of users.\nWithout pagination, the flow may still:\ncomplete successfully; return HTTP 200; process valid users; update SharePoint or Dataverse; send notifications. The problem is that it may have processed only part of the dataset.\nThis makes pagination particularly important for administrative automation such as:\nuser lifecycle management; inactive account reporting; licence reporting; application and service principal inventories; device inventories; group membership processing; SharePoint reporting; security and governance automation. The pattern I use # For Power Automate, I normally use a Do until loop controlled by a variable containing the URL for the next Graph request.\nThe basic structure looks like this:\nInitialise variables ↓ Set Graph URL ↓ Do until Graph URL is empty ↓ Call Microsoft Graph ↓ Process response.value ↓ Set Graph URL = @odata.nextLink ↓ Repeat Once Graph stops returning @odata.nextLink, the URL variable becomes empty and the loop ends.\nStep 1 - Initialise the variables # Create a string variable:\nName: varGraphUrl Type: String Set its initial value to the Graph endpoint you want to query.\nFor example:\nhttps://graph.microsoft.com/v1.0/users I also commonly create an array variable:\nName: varResults Type: Array Value: [] This is useful when I need to collect all records before processing them later.\nWhether an array variable is necessary depends on the flow.\nFor large datasets, processing each page immediately can be more efficient than building one very large array in memory.\nStep 2 - Create the Do until loop # Add a Do until action.\nThe loop should continue until varGraphUrl is empty.\nOne way of expressing the condition is:\nempty(variables(\u0026#39;varGraphUrl\u0026#39;)) The loop therefore finishes once this expression evaluates to:\ntrue At the beginning of the process varGraphUrl contains the initial Graph endpoint, so the loop starts normally.\nStep 3 - Call Microsoft Graph # Inside the loop, make the HTTP request using the URL stored in the variable.\nConceptually:\nGET @{variables(\u0026#39;varGraphUrl\u0026#39;)} The exact HTTP action depends on how the flow authenticates to Microsoft Graph.\nFor example, the request might be made through:\nHTTP with Microsoft Entra ID; an authenticated custom connector; another authorised Graph integration. The important part of the pagination design is that the request URI comes from varGraphUrl.\nFor the first iteration this will contain the original endpoint.\nFor later iterations it will contain the URL returned in @odata.nextLink.\nStep 4 - Read the records from value # Microsoft Graph normally returns collections inside the value property.\nFor example:\n{ \u0026#34;value\u0026#34;: [ { \u0026#34;id\u0026#34;: \u0026#34;1\u0026#34;, \u0026#34;displayName\u0026#34;: \u0026#34;User One\u0026#34; }, { \u0026#34;id\u0026#34;: \u0026#34;2\u0026#34;, \u0026#34;displayName\u0026#34;: \u0026#34;User Two\u0026#34; } ] } The array can normally be referenced with an expression similar to:\nbody(\u0026#39;HTTP\u0026#39;)?[\u0026#39;value\u0026#39;] You can then either:\nProcess each page immediately # Use an Apply to each against:\nbody(\u0026#39;HTTP\u0026#39;)?[\u0026#39;value\u0026#39;] and perform whatever work is required for each object.\nThis is often my preferred approach when the records do not need to be combined into one large dataset.\nOr collect the results # If the complete dataset is required later in the flow, the records can be added to an array.\nThe exact implementation depends on what will be done with the results afterwards.\nStep 5 - Capture @odata.nextLink # After processing the current page, update varGraphUrl.\nUse:\nbody(\u0026#39;HTTP\u0026#39;)?[\u0026#39;@odata.nextLink\u0026#39;] However, I prefer to handle the possibility that the property does not exist.\nA safer expression is:\ncoalesce( body(\u0026#39;HTTP\u0026#39;)?[\u0026#39;@odata.nextLink\u0026#39;], \u0026#39;\u0026#39; ) This does two things.\nIf another page exists:\nvarGraphUrl = https://graph.microsoft.com/... If there are no more pages:\nvarGraphUrl = \u0026#39;\u0026#39; The Do until condition then detects the empty string and finishes.\nComplete flow logic # The completed pattern can be represented as:\nInitialise varGraphUrl | |-- https://graph.microsoft.com/v1.0/users | Initialise varResults | |-- [] | Do until: empty(varGraphUrl) | +-- HTTP GET | URI = varGraphUrl | +-- Process body(\u0026#39;HTTP\u0026#39;)?[\u0026#39;value\u0026#39;] | +-- Set varGraphUrl | +-- coalesce( body(\u0026#39;HTTP\u0026#39;)?[\u0026#39;@odata.nextLink\u0026#39;], \u0026#39;\u0026#39; ) Graph controls the pagination and Power Automate simply follows the links until no more remain.\nDo not construct the next URL manually # One important rule is:\nUse the URL returned by @odata.nextLink.\nDo not try to extract the paging token and reconstruct the URL yourself unless there is a specific technical reason to do so.\nFor example, Graph may return something similar to:\nhttps://graph.microsoft.com/v1.0/users?$skiptoken=RFNwdAIAAQAA... The safest approach is simply:\nGET \u0026lt;the entire @odata.nextLink value\u0026gt; The paging information should generally be treated as opaque.\nUsing $select # If I only need a few properties, I reduce the response size with $select.\nInstead of:\nGET https://graph.microsoft.com/v1.0/users I might use:\nGET https://graph.microsoft.com/v1.0/users?$select=id,displayName,userPrincipalName,accountEnabled This can significantly reduce unnecessary data being transferred and processed by the flow.\nIt also makes the returned JSON easier to work with.\nPagination works in exactly the same way.\nGraph includes the appropriate query information in the next link it returns.\nUsing $top # Some Microsoft Graph APIs allow the requested page size to be influenced using $top.\nFor example:\nGET https://graph.microsoft.com/v1.0/users?$top=500 It is important not to confuse $top with pagination.\nRequesting a larger page does not mean:\nReturn every record in the tenant.\nIt means:\nRequest up to this number of records for this page, where supported.\nGraph may still return an @odata.nextLink.\nThe flow should therefore continue checking for pagination regardless of the requested page size.\nA reusable expression # One of the most useful expressions in this pattern is:\ncoalesce( body(\u0026#39;HTTP\u0026#39;)?[\u0026#39;@odata.nextLink\u0026#39;], \u0026#39;\u0026#39; ) Using the safe-navigation operator:\n?[\u0026#39;@odata.nextLink\u0026#39;] means the flow does not fail simply because the property is absent.\ncoalesce() then converts the missing value into an empty string which can be used by the loop condition.\nProcessing records page-by-page vs storing everything # There are two common approaches.\nOption 1 - Process each page immediately # Graph page ↓ Apply to each ↓ Perform actions ↓ Get next page I generally prefer this approach for large datasets.\nAdvantages include:\nless data held in flow variables; simpler memory usage; processing starts immediately; easier handling of large collections. For example, a user lifecycle flow could retrieve one page of accounts, evaluate those users, then continue to the next page.\nOption 2 - Build one complete array # Graph page 1 ─┐ Graph page 2 ─┤ Graph page 3 ─┼── Complete array Graph page 4 ─┘ This can be useful when later steps require the entire dataset.\nExamples might include:\ngenerating a single report; comparing records across the complete collection; passing all results to another service; producing a consolidated output. The trade-off is that very large arrays can make Power Automate flows more resource-intensive and harder to troubleshoot.\nHandling throttling # Pagination can result in many Graph requests, particularly in large environments.\nMicrosoft Graph may throttle requests and return:\n429 Too Many Requests Where appropriate, production flows should account for this.\nThe response may include a Retry-After header indicating how long the caller should wait before retrying.\nOther useful design considerations include:\navoiding unnecessary Graph calls; retrieving only required properties with $select; avoiding excessive nested HTTP requests; using sensible Power Automate retry policies; limiting concurrency where appropriate. Pagination solves the problem of retrieving all records, but the wider flow still needs to be designed with Graph service limits in mind.\nCheck the Do until limits # A Power Automate Do until action has execution limits.\nIf a Graph query could require a large number of pages, I check the loop configuration rather than relying blindly on the defaults.\nThis is particularly important when using small Graph page sizes against large directories.\nA flow can otherwise stop because the Power Automate loop limit has been reached rather than because Graph has finished returning data.\nCommon mistakes # Only reading the first value array # This is probably the easiest pagination issue to introduce.\nHTTP → Parse JSON → Apply to each may look completely correct while processing only the first page.\nAlways check the Graph response for:\n@odata.nextLink Assuming $top disables pagination # It does not.\nEven when $top is specified, continue checking for @odata.nextLink.\nCreating paging tokens manually # Avoid trying to manipulate $skiptoken values.\nUse the complete URL Graph provides.\nCollecting thousands of records unnecessarily # If each user can be processed independently, processing page-by-page is usually cleaner than building a massive array first.\nForgetting about throttling # A flow that works against 50 test accounts may behave very differently against several thousand production accounts.\nPagination, API request volume and retry behaviour should be considered together.\nExample: retrieving all enabled users # As an example, the initial query could be:\nhttps://graph.microsoft.com/v1.0/users?$select=id,displayName,userPrincipalName,accountEnabled The flow starts with:\nvarGraphUrl = https://graph.microsoft.com/v1.0/users?$select=id,displayName,userPrincipalName,accountEnabled Inside the loop:\nHTTP GET → variables(\u0026#39;varGraphUrl\u0026#39;) Process:\nbody(\u0026#39;HTTP\u0026#39;)?[\u0026#39;value\u0026#39;] Then update the URL:\ncoalesce( body(\u0026#39;HTTP\u0026#39;)?[\u0026#39;@odata.nextLink\u0026#39;], \u0026#39;\u0026#39; ) The cycle continues:\nRequest page 1 ↓ Process records ↓ @odata.nextLink exists ↓ Request page 2 ↓ Process records ↓ @odata.nextLink exists ↓ Request page 3 ↓ Process records ↓ No @odata.nextLink ↓ Flow continues This pattern can be reused for many Microsoft Graph collection endpoints.\nA useful troubleshooting technique # When building the flow, I normally inspect the raw response from the HTTP action before adding the full pagination logic.\nI look specifically for:\n\u0026#34;value\u0026#34;: [...] and:\n\u0026#34;@odata.nextLink\u0026#34;: \u0026#34;...\u0026#34; If @odata.nextLink is present, I know immediately that the current response is not the complete dataset.\nThis is also useful when troubleshooting an existing automation which appears to be missing records.\nFinal design # The core pagination logic is surprisingly small:\n1. Store the initial Graph URL. 2. Call Graph. 3. Process: body(\u0026#39;HTTP\u0026#39;)?[\u0026#39;value\u0026#39;] 4. Store: coalesce( body(\u0026#39;HTTP\u0026#39;)?[\u0026#39;@odata.nextLink\u0026#39;], \u0026#39;\u0026#39; ) 5. Repeat until the URL is empty. The important part is not the complexity of the solution, but recognising that pagination needs to be handled in the first place.\nFor small test datasets, ignoring pagination may appear to work perfectly. In larger Microsoft 365 environments, however, correctly following @odata.nextLink is essential if an automation needs to reliably process the complete dataset.\nKey takeaway # Whenever I build a Power Automate flow that retrieves a collection from Microsoft Graph, one of the first things I check is:\nDoes this response contain @odata.nextLink? If it does, I treat pagination as part of the API integration rather than an optional enhancement.\nThat small design decision prevents an automation from silently processing only part of the environment.\n","date":"21 September 2026","externalUrl":null,"permalink":"/guides/handling-pagination-graph-powerautomate/","section":"Guides","summary":"","title":"Handling Pagination When Calling Microsoft Graph from Power Automate","type":"guides"},{"content":"","date":"21 September 2026","externalUrl":null,"permalink":"/tags/identity-governance/","section":"Tags","summary":"","title":"Identity Governance","type":"tags"},{"content":"","date":"21 September 2026","externalUrl":null,"permalink":"/tags/microsoft-teams/","section":"Tags","summary":"","title":"Microsoft Teams","type":"tags"},{"content":"","date":"21 September 2026","externalUrl":null,"permalink":"/tags/power-bi/","section":"Tags","summary":"","title":"Power BI","type":"tags"},{"content":"","date":"21 September 2026","externalUrl":null,"permalink":"/tags/powershell/","section":"Tags","summary":"","title":"PowerShell","type":"tags"},{"content":"A selection of projects demonstrating how I\u0026rsquo;ve used Microsoft technologies to solve operational and engineering challenges.\n","date":"21 September 2026","externalUrl":null,"permalink":"/projects/","section":"Projects","summary":"","title":"Projects","type":"projects"},{"content":" The challenge # Microsoft 365 provides useful SharePoint usage reports, but the available reporting experience is not always suited to longer-term analysis or organisation-specific reporting requirements.\nThe requirement was to create a dataset that could preserve daily SharePoint usage snapshots and support management reporting across different time periods.\nThis included identifying popular sites, analysing changes in usage and understanding longer-term activity trends.\nThe solution # I designed a scheduled data pipeline that retrieves SharePoint site usage information from Microsoft Graph and stores historical daily snapshots in Dataverse.\nBecause the Graph reporting endpoint returns report data as CSV rather than conventional JSON, the process includes an Office Script transformation stage before the information is processed by Power Automate.\nThe resulting structured dataset is consumed by Power BI for historical and comparative reporting.\nArchitecture # The high-level pipeline is:\nMicrosoft Graph Reports → CSV usage report → Office Script transformation → Power Automate → Dataverse → Power BI\nThe data model separates relatively static site information from daily usage measurements.\nThis provides a structure similar to:\nSite\nSite identifier Site name Site URL Site type resolution status Site Usage\nSite identifier usage date last activity date page views pages visited file count active file count storage consumption This avoids unnecessarily duplicating site metadata for every reporting snapshot.\nSite resolution # Usage reports primarily identify sites using their site identifier.\nA separate resolution process enriches those records with readable information such as the SharePoint site name and URL.\nKeeping this process separate from the daily usage ingestion means reporting can continue even when a particular site\u0026rsquo;s metadata cannot immediately be resolved.\nAnalytics # Once historical snapshots are available, Power BI can calculate measures that are difficult to obtain from a single Microsoft 365 usage export.\nExamples include:\nusage over selectable reporting periods most frequently visited sites page-view trends recent average activity site rankings largest increases in usage largest decreases in usage comparison of current and historical activity Users can analyse activity across different reporting windows without needing separate exports for each period.\nData pipeline design # The reporting process is designed to run without manual intervention.\nEach scheduled execution retrieves the required reporting snapshot, transforms the source data and adds the corresponding usage records to the historical dataset.\nSeparating ingestion, site metadata and analytics also makes the solution easier to troubleshoot and extend.\nAdditional SharePoint metrics can be introduced without requiring the entire reporting architecture to be redesigned.\nOutcome # The solution converts a point-in-time Microsoft 365 usage report into a persistent analytics platform.\nHistorical data can be used to identify usage patterns, monitor adoption and provide management with a clearer view of how SharePoint sites are being used over time.\nThe design also creates a reusable pattern for capturing Microsoft 365 reporting data that would otherwise only be available through periodically generated reports.\nTechnologies # SharePoint Online · Microsoft Graph · Power Automate · Office Scripts · Dataverse · Power BI\n","date":"21 September 2026","externalUrl":null,"permalink":"/projects/sharepoint-usage-analytics/","section":"Projects","summary":"Building a historical SharePoint analytics platform using Microsoft Graph reporting, Power Automate, Dataverse and Power BI.","title":"SharePoint Usage Analytics \u0026 Power BI","type":"projects"},{"content":" The challenge # Operational teams needed to regularly change telephone routing used by Microsoft Teams auto attendants.\nPreviously, making these changes required intervention from users with administrative access to Microsoft Teams. This created an unnecessary dependency on technical teams for what was fundamentally an operational change.\nThe goal was to provide authorised staff with a simple self-service interface while keeping privileged Teams administration securely behind the solution.\nThe solution # I designed a Power Platform solution that allows authorised users to select or enter a duty contact number through a Power Apps application.\nThe application submits the requested change through Power Automate, which validates and records the request before securely triggering an Azure Automation runbook.\nThe runbook performs the privileged Microsoft Teams configuration change and verifies that the requested number has been successfully applied.\nArchitecture # The solution uses:\nPower Apps for the user-facing interface SharePoint for contact configuration, current state and audit logging Power Automate for orchestration and notifications Azure Automation for privileged Teams administration Microsoft Teams PowerShell for updating and verifying auto-attendant configuration The high-level flow is:\nPower Apps\n→ Power Automate\n→ Azure Automation\n→ Microsoft Teams\n→ Verification\n→ Power Automate\n→ User notification\nValidation and safeguards # The solution includes several controls designed to prevent incorrect changes:\nTelephone number format validation before submission Authorised-user access controlled through security groups Unique request identifiers for each change Central job logging for troubleshooting and audit Post-change verification against Microsoft Teams Success and failure notifications Separation between the user-facing application and privileged administrative operations Operational design # Contacts can be maintained centrally so users can select the appropriate duty person without repeatedly entering telephone numbers manually.\nConfiguration and job-log data also provide support teams with visibility of:\nwho requested the change the requested telephone number when the request was submitted whether the change succeeded the value returned during verification This allows most support issues to be investigated without requiring access to the automation infrastructure.\nOutcome # The solution converted an administrator-dependent process into a controlled self-service service.\nAuthorised operational users can now manage routine telephony changes themselves while privileged Microsoft Teams administration remains isolated behind the automation layer.\nThe design also provides validation, logging and verification that were difficult to achieve consistently with the previous manual process.\nTechnologies # Power Apps · Power Automate · SharePoint · Azure Automation · PowerShell · Microsoft Teams\n","date":"21 September 2026","externalUrl":null,"permalink":"/projects/teams-telephony-automation/","section":"Projects","summary":"Replacing an administrator-dependent Teams telephony process with a secure self-service solution built with Power Apps, Power Automate and Azure Automation.","title":"Teams Telephony Self-Service Automation","type":"projects"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"}]