Click here to book an initial consultation!

Microsoft Loop to HTML Export — Technical Guide

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=html parameter 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.Selected permission 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.All alone 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 curl, jq, bash

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

Files.Read.All

Application

Read files in all site collections

Yes

Sites.Read.All

Application

Read items in all site collections

Yes

FileStorageContainer.Selected

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: Always a187e399-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

(403) Forbidden on Connect-SPOService

User is not SharePoint Admin or Global Admin

Assign the correct role in Entra ID

Attempted to perform an unauthorized operation

User is SharePoint Admin but not Global Admin

Use a Global Admin account

Invalid AppOnlyPermissions: ["ReadContent"]

Wrong casing

Use lowercase: "readcontent"

Delegated permissions not supported

SPE limitation

Omit -PermissionDelegated parameter

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. Check moreResultsAvailable in 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=html parameter triggers server-side conversion from .loop format 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 .loop file (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:

  1. Files.Read.All (Application) — for Search API

  2. FileStorageContainer.Selected (Application) — for SPE containers

  3. 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

Files.Read.All (Application) without SPE registration

Search works, drive access 403

Files.Read.All (Delegated via Device Code)

Azure AD rejects device code for confidential clients

az login token for SharePoint Admin REST API

Wrong scope (user_impersonation not granted to Azure CLI)

Connect-SPOService on macOS

"Object reference not set to an instance of an object"

Set-SPOApplication PowerShell cmdlet

Does not have -ApplicationId parameter — wrong cmdlet

Graph API /admin/sharepoint/settings/containerTypes/

Endpoint does not exist (neither v1.0 nor beta)

Container listing via /storage/fileStorage/containers

403 without SPE registration


Reference