Microsoft Loop to HTML Export: Technical Guide
How to export all Microsoft Loop workspace pages as HTML files via the Microsoft Graph API.
Introduction
Microsoft Loop is an attractive tool for collaborative note-taking. It is included in most Microsoft 365 plans at no extra cost, integrates natively with Teams and Outlook, and does its job for meeting notes, project docs and knowledge bases. For small and mid-size organizations that are paying for M365 anyway, it is hard to justify the cost of a dedicated tool like Notion or Confluence when Loop is simply there.
Until you want out.
The problem
Loop has no export API. There is no "Export Workspace" button. No bulk download. No documented programmatic access to your own content. When we wanted to migrate our workspace (over 1,000 Loop pages, accumulated over two years of daily use), it quickly became clear that the obvious paths are all dead ends:
Manual copy-paste: Not feasible with 1,000+ pages. Even at 2 minutes per page, that is over 33 hours of mindless work.
PDF export: Loop offers an "Export to PDF" per page. The output loses all the structure, links and formatting that would make it usable in a target system. For a migration, PDF is a format to die in, not to start from.
Power Automate: Various community posts suggest Power Automate flows to access Loop content. We tried it. The flows confirmed that Loop files can be reached programmatically through SharePoint's storage layer, but Power Automate's per-file trigger model and throttling limits make it impractical for bulk exports. What it did give us was the crucial hint: Loop pages are stored in SharePoint Embedded containers and are accessible via the Microsoft Graph API.
That hint sent us down a rabbit hole of Entra ID app registrations, undocumented permission models and PowerShell cmdlets that only work on Windows. This guide is what we found on the other side.
Acknowledgments
We did not figure this out from scratch. Crucial pointers came from the community:
Marco De Toni's question on Microsoft Q&A confirmed that the Graph API's
?format=htmlparameter works for Loop files in SharePoint Embedded, the crucial puzzle piece that makes a clean export possible in the first place.Various blog posts on SharePoint Embedded container types and the
FileStorageContainer.Selectedpermission model guided us through the otherwise undocumented SPE guest registration process.If you have written about Loop's internal storage architecture or SPE permissions: thank you. Your breadcrumbs saved us days of guesswork.
Export script
The complete export script (including the shared library) is available as a GitHub gist: export-loop.sh
Overview
Microsoft Loop stores workspace pages in SharePoint Embedded (SPE) containers. These are not regular SharePoint sites. They use a separate permission model that requires an explicit app registration on the Loop container type.
Architecture
Discovery: Search API (/search/query) <- Client Credentials Token (Files.Read.All Application Permission) Download: Drive API (/drives/{id}/...) <- Client Credentials Token (FileStorageContainer.Selected + SPE Guest Registration) Export: ?format=html Query Parameter <- Graph API converts .loop -> HTML on the flyKey Insight
The Search API can discover Loop files across SPE containers with just
Files.Read.AllBut direct drive/item access (metadata, content download) requires the app to be registered as a guest application on Loop's container type via
Set-SPOApplicationPermission
Files.Read.Allalone is not sufficient for SPE container content. This is a documented Microsoft limitation
Prerequisites
Component
Purpose
Azure AD App Registration
Client Credentials auth for Graph API
Global Admin access (one-time)
Register the app on the Loop container type
Windows PC with PowerShell (one-time)
SharePoint Online Management Shell
macOS/Linux with
curl,jq,bashRun the export script
Step 1: App Registration in Entra ID
Create a new app registration (or use an existing one):
Name: e.g. "Loop Export"
Supported account types: Single Tenant ("My organization only")
Platform: Add a "Mobile and desktop applications" redirect URI (required for public client flows)
API Permissions (Microsoft Graph — Application)
Permission
Type
Description
Admin Consent
Files.Read.AllApplication
Read files in all site collections
Yes
Sites.Read.AllApplication
Read items in all site collections
Yes
FileStorageContainer.SelectedApplication
Access selected file storage containers
Yes
Grant admin consent for all three permissions.
Client Secret
Create a client secret under "Certificates & secrets". Note the value: it cannot be retrieved afterwards.
Allow Public Client Flows
Under "Authentication" > "Advanced settings":
Set "Allow public client flows" to Yes
This is needed if you want to use the Device Code Flow for delegated access. For the pure Client Credentials export it is optional, but recommended.
Note the values
Application (client) ID: <your-client-id> Directory (tenant) ID: <your-tenant-id> Client Secret Value: <your-secret>Store them in an
.envfile:LOOP_CLIENT_ID=<your-client-id> LOOP_CLIENT_SECRET=<your-secret> LOOP_TENANT_ID=<your-tenant-id>
Step 2: SPE Guest App Registration (requires Global Admin)
This is the critical step. Without it, the app can discover Loop files via Search, but cannot download their content.
Why this is necessary
Loop uses SharePoint Embedded (SPE) containers with the container type ID
a187e399-0c36-4b98-8f04-1edc167a0996. This is a Microsoft-owned container type. Third-party apps must be explicitly registered as "guest applications" to be able to access content in these containers.Procedure
On a Windows PC (the SharePoint Online Management Shell does not work reliably on macOS):
# 1. Install the module (one-time) Install-Module Microsoft.Online.SharePoint.PowerShell # 2. Connect as Global Admin Connect-SPOService -Url "https://<tenant>-admin.sharepoint.com" # 3. Register the app as a guest on Loop's container type Set-SPOApplicationPermission -OwningApplicationId "a187e399-0c36-4b98-8f04-1edc167a0996" -GuestApplicationId "<your-client-id>" -PermissionAppOnly "readcontent"Important notes
-OwningApplicationId: Alwaysa187e399-0c36-4b98-8f04-1edc167a0996(Loop's container type, identical for all tenants)
-GuestApplicationId: The client ID of the app from Step 1
-PermissionAppOnly: Use lowercase"readcontent"(not"ReadContent")Delegated permissions: Currently not supported by SPE. Omit the
-PermissionDelegatedparameterRequired role: Global Admin. SharePoint Admin alone is not sufficient for this cmdlet
One-time operation: Only needs to be done once per app registration
Common errors
Error
Cause
Fix
(403) ForbiddenonConnect-SPOServiceUser is not a SharePoint Admin or Global Admin
Assign the correct role in Entra ID
Attempted to perform an unauthorized operationUser is a SharePoint Admin, but not a Global Admin
Use a Global Admin account
Invalid AppOnlyPermissions: ["ReadContent"]Wrong spelling
Use lowercase:
"readcontent"
Delegated permissions not supportedSPE limitation
Omit the
-PermissionDelegatedparameterBacktick line continuation does not work
PowerShell parsing issue
Paste as a single line
Step 3: Run the export
How the export script works
1. Authenticate via Client Credentials (OAuth2 client_credentials Grant) 2. Discover all .loop/.fluid files via POST /search/query - Uses the region parameter (e.g. "EUR") — required for application permissions - Paginates through all results (25 per page) 3. For each file: a. Download HTML via GET /drives/{driveId}/items/{itemId}/content?format=html b. Save metadata (author, dates, permissions) as JSON c. Fall back to raw .loop download if HTML conversion fails 4. Write manifest.json with the complete inventoryDrive ID Encoding
Loop drive IDs contain
!(e.g.b!a2IBG9x...). In Graph API URLs, the!must be URL-encoded as%21:GET /drives/b%21a2IBG9x.../items/{itemId}/contentThe
--path-as-iscurl flag prevents curl from normalizing the URL.Usage
# Dry run — discover files without downloading ./tools/export-loop.sh --dry-run # Full export (idempotent — skips files that have already been exported) ./tools/export-loop.sh # Force re-export of all files ./tools/export-loop.sh --forceOutput structure
exports/loop/ manifest.json # Complete inventory with status per file pages/ Filename__itemId.html # HTML export of each Loop page meta/ Filename__itemId.json # Metadata (author, dates, permissions)Token Refresh
The script automatically refreshes the Client Credentials token every 45 minutes. For exports with 700+ files, this happens transparently during the run.
API Details
Search API (Discovery)
POST https://graph.microsoft.com/v1.0/search/query Authorization: Bearer <client-credentials-token> Content-Type: application/json { "requests": [{ "entityTypes": ["driveItem"], "query": { "queryString": "filetype:loop OR filetype:fluid" }, "from": 0, "size": 25, "region": "EUR" }] }
region: Required for application permissions."EUR"for European tenants,"NAM"for North America, etc.Pagination: Use
from+size. CheckmoreResultsAvailablein the response.Returns: File name, driveId, itemId, size, dates, authors, webUrl, parentReference
HTML Download
GET https://graph.microsoft.com/v1.0/drives/{driveId}/items/{itemId}/content?format=html Authorization: Bearer <client-credentials-token>
The
?format=htmlparameter triggers server-side conversion from the.loopformat to HTMLFollows redirects (with
curl -L)Returns complete HTML with inline styles
Raw Download (Fallback)
GET https://graph.microsoft.com/v1.0/drives/{driveId}/items/{itemId}/content Authorization: Bearer <client-credentials-token>
Downloads the raw
.loopfile (Fluid Framework format)Useful if HTML conversion fails for certain files
Troubleshooting
"403 Forbidden" on drive/item access
The app is not registered as an SPE guest. Perform Step 2.
Search returns files, but download fails
Same cause. The Search API has special cross-container access that bypasses SPE restrictions, but direct drive access does not.
"Resource not found for segment '!...'"
The
!in drive IDs is being URL-decoded. Make sure it is encoded as%21."File name too long" during export
Some Loop page names exceed filesystem limits (255 bytes on macOS). The export script shortens filenames to 100 characters.
Client Credentials token works for search, but not for downloads
Make sure all three permissions are granted:
Files.Read.All(Application) — for the Search API
FileStorageContainer.Selected(Application) — for SPE containersSPE guest registration via
Set-SPOApplicationPermission— links the permission to Loop's container typeDevice Code Flow fails with "client_assertion or client_secret required"
Azure AD does not support the Device Code Flow for confidential clients (apps with secrets). Use the Client Credentials flow instead, which works after SPE guest registration.
What does NOT work
These approaches were tested and confirmed not to work for Loop SPE containers:
Approach
Result
Files.Read.All(Application) without SPE registrationSearch works, drive access 403
Files.Read.All(Delegated via Device Code)Azure AD rejects Device Code for confidential clients
az logintoken for the SharePoint Admin REST APIWrong scope (
user_impersonationnot granted to Azure CLI)
Connect-SPOServiceon macOS"Object reference not set to an instance of an object"
Set-SPOApplicationPowerShell cmdletHas no
-ApplicationIdparameter: wrong cmdletGraph API
/admin/sharepoint/settings/containerTypes/Endpoint does not exist (neither v1.0 nor beta)
Container listing via
/storage/fileStorage/containers403 without SPE registration
References
Loop Container Type ID:
a187e399-0c36-4b98-8f04-1edc167a0996(constant across all tenants)SharePoint Embedded Docs: https://learn.microsoft.com/en-us/sharepoint/dev/embedded/overview
Graph Search API: https://learn.microsoft.com/en-us/graph/search-concept-files
SPO PowerShell Module:
Microsoft.Online.SharePoint.PowerShell(v16.0.27011+)