How to export all Microsoft Loop workspace pages as HTML files using the Microsoft Graph API.
Introduction
Microsoft Loop is an attractive choice for collaborative note-taking. It's included in most Microsoft 365 plans at no extra cost, integrates natively with Teams and Outlook, and gets the job done for meeting notes, project docs, and knowledge bases. For small to mid-size organizations already paying for M365, it's hard to justify the cost of a dedicated tool like Notion or Confluence when Loop is just there.
Until you need to leave.
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 decided to migrate our workspace — over 1,000 Loop pages accumulated across two years of daily use — we quickly realized that the obvious paths were all dead ends:
-
Manual copy-paste: Not feasible at 1,000+ pages. Even at 2 minutes per page, that's over 33 hours of mindless work.
-
PDF export: Loop offers a per-page "Export to PDF" option. The output loses all 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: Several community posts suggest using Power Automate flows to access Loop content. We explored this route — the flows confirmed that Loop files can be accessed programmatically through SharePoint's storage layer, but Power Automate's per-file trigger model and throttling limits make it impractical for bulk export. What it did give us was the crucial hint: Loop pages are stored in SharePoint Embedded containers and can be accessed 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 didn't figure this out from scratch. Key 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 stored in SharePoint Embedded — the critical piece that makes a clean export possible at all. -
Various blog posts on SharePoint Embedded container types and the
FileStorageContainer.Selectedpermission model helped us navigate the otherwise underdocumented SPE guest registration process.
If you've 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 is available for download: 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 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 fly
Key Insight
-
The Search API can discover Loop files across SPE containers with just
Files.Read.All -
But 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 app on Loop container type |
|
Windows PC with PowerShell (one-time) |
SharePoint Online Management Shell |
|
macOS/Linux with |
Running 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 |
|---|---|---|---|
|
|
Application |
Read files in all site collections |
Yes |
|
|
Application |
Read items in all site collections |
Yes |
|
|
Application |
Access selected file storage containers |
Yes |
Grant admin consent for all three permissions after adding them.
Client Secret
Create a client secret under "Certificates & secrets". Note the value — it cannot be retrieved later.
Allow Public Client Flows
Under "Authentication" > "Advanced settings":
-
Set "Allow public client flows" to Yes
This is needed if you ever want to use Device Code Flow for delegated access. For the pure Client Credentials export, it's optional but recommended.
Values to note
Application (client) ID: <your-client-id>
Directory (tenant) ID: <your-tenant-id>
Client Secret Value: <your-secret>
Store these in a .env file:
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 this, the app can discover Loop files via Search but cannot download their content.
Why this is needed
Loop uses SharePoint Embedded (SPE) containers with 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 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 your app as 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, same for all tenants) -
-GuestApplicationId: Your app's client ID from Step 1 -
-PermissionAppOnly: Use lowercase"readcontent"(not"ReadContent") -
Delegated permissions: Not supported by SPE at this time — omit
-PermissionDelegated -
Role required: Global Admin. SharePoint Admin alone is not sufficient for this cmdlet
-
One-time operation: This only needs to be done once per app registration
Common errors
|
Error |
Cause |
Fix |
|---|---|---|
|
|
User is not SharePoint Admin or Global Admin |
Assign the correct role in Entra ID |
|
|
User is SharePoint Admin but not Global Admin |
Use a Global Admin account |
|
|
Wrong casing |
Use lowercase: |
|
|
SPE limitation |
Omit |
|
Backtick line continuation doesn't work |
PowerShell parsing issue |
Paste as a single line |
Step 3: Running 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 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 full inventory
Drive ID encoding
Loop drive IDs contain ! (e.g. b!a2IBG9x...). When used in Graph API URLs, the ! must be URL-encoded as %21:
GET /drives/b%21a2IBG9x.../items/{itemId}/content
The --path-as-is curl flag prevents curl from normalizing the URL.
Usage
# Dry run — discover files without downloading
./tools/export-loop.sh --dry-run
# Full export (idempotent — skips already-exported files)
./tools/export-loop.sh
# Force re-export of all files
./tools/export-loop.sh --force
Output structure
exports/loop/
manifest.json # Full 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. Use"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.loopformat to HTML -
Follows redirects (use
curl -L) -
Returns full 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 specific files
Troubleshooting
"403 Forbidden" on drive/item access
The app is not registered as SPE guest. Complete Step 2.
Search returns files but download fails
Same cause — 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. Ensure it's encoded as %21.
"File name too long" during export
Some Loop page names exceed filesystem limits (255 bytes on macOS). The export script truncates filenames to 100 characters.
Client Credentials token works for search but not downloads
Verify all three permissions are granted:
-
Files.Read.All(Application) — for Search API -
FileStorageContainer.Selected(Application) — for SPE containers -
SPE guest registration via
Set-SPOApplicationPermission— links the permission to Loop's container type
Device Code Flow fails with "client_assertion or client_secret required"
Azure AD does not support Device Code Flow for confidential clients (apps with secrets). Use Client Credentials flow instead, which works after SPE guest registration.
What does NOT work
These approaches were tested and confirmed non-functional for Loop SPE containers:
|
Approach |
Result |
|---|---|
|
|
Search works, drive access 403 |
|
|
Azure AD rejects device code for confidential clients |
|
|
Wrong scope ( |
|
|
"Object reference not set to an instance of an object" |
|
|
Does not have |
|
Graph API |
Endpoint does not exist (neither v1.0 nor beta) |
|
Container listing via |
403 without SPE registration |
Reference
-
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+)