Click here to book a no-obligation intro call!

Posted by Markus Wals

Microsoft Loop to HTML Export: Technical Guide

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=html parameter 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.Selected permission 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 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 the app on the Loop container type

Windows PC with PowerShell (one-time)

SharePoint Online Management Shell

macOS/Linux with curl, jq, bash

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

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 .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 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: Always a187e399-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 -PermissionDelegated parameter

  • Required 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) Forbidden on Connect-SPOService

User is not a SharePoint Admin or Global Admin

Assign the correct role in Entra ID

Attempted to perform an unauthorized operation

User 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 supported

SPE limitation

Omit the -PermissionDelegated parameter

Backtick 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 inventory

Drive 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}/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 files that have already been exported)
./tools/export-loop.sh

# Force re-export of all files
./tools/export-loop.sh --force

Output 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. 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 the .loop format to HTML

  • Follows 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 .loop file (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:

  1. Files.Read.All (Application) — for the 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 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 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 the 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

Has no -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


References