FlowLibs
Browse Cloud FlowsConnectorsAI ToolsDev ToolsPricingAboutContact
Dev Tools/SharePoint

SharePoint Patterns

A working reference for building on SharePoint Online — the REST/OData API surface, list-item CRUD with the right headers, querying and expanding lookup/person fields, surviving the 5,000-item list view threshold with column indexing, throttling and $batch, unique permissions and role assignments, document-library file handling (including chunked large-file upload), and the Power Automate SharePoint connector patterns that pull it all together.

SharePoint26 min read·Updated 2026-06-16
SharePointSharePoint OnlineRESTODataMicrosoft ListsLarge ListsList View ThresholdIndexingPermissionsDocument LibrariesPower AutomateThrottlingMicrosoft Graph

On this page

  • Three ways in — pick the right one
  • The REST/OData surface
  • OData querying: $select, $filter, $expand, paging
  • Creating, updating & deleting items
  • Writing the awkward field types
  • Large lists & the 5,000-item threshold
  • Throttling: 429, 503 & Retry-After
  • $batch — combine requests
  • Permissions: inheritance, roles & unique scopes
  • Document libraries & files
  • Power Automate connector patterns
  • Field notes & gotchas

Three ways in — pick the right one

Almost everything in SharePoint is reachable three ways, and choosing well saves you most of the pain. Reach for the first-party SharePoint connector actions (Get items, Create item, Create file…) for the common 80%; drop to Send an HTTP request to SharePoint when the connector can’t express what you need; and prefer Microsoft Graph for cross-service work and lighter throttling.

ApproachBest forAuth & cost
SharePoint connector actionsStandard list/library CRUD inside Power Automate / Power AppsStandard connector — handles auth for you
Send an HTTP request to SharePointAnything the connector misses — permissions, folders, batch, admin RESTStandard — runs SharePoint REST as the flow owner, digest handled
Microsoft Graph (/sites, /drives)Cross-service, delta sync, app-only, high volumePremium HTTP / Graph — fewer resource units, less throttling

Graph is cheaper at scale

Microsoft’s own guidance is to choose Graph over CSOM and REST when possible — Graph consumes fewer resource units for the same work, so it throttles later. Keep SharePoint REST for the things Graph still can’t do (some field types, classic permissions, certain admin endpoints).

Internal names ≠ display names

Every column has a display name (what you see) and an internal name (what the API uses). Renaming a column changes only the display name; the internal name is frozen at creation, and spaces become _x0020_ (e.g. “Start Date” → Start_x0020_Date). Always query and write by internal name.

The REST/OData surface

The SharePoint REST service lives under _api/ on every site. List data hangs off _api/web. Address a list by title, by GUID, or by its server-relative URL — title is readable but breaks if the list is renamed, so GUID is the stable choice for production.

http
GET https://contoso.sharepoint.com/sites/Ops/_api/web/lists/getbytitle('Orders')/items
GET https://contoso.sharepoint.com/sites/Ops/_api/web/lists(guid'2b1c…')/items
GET https://contoso.sharepoint.com/sites/Ops/_api/web/GetList('/sites/Ops/Lists/Orders')/items
Accept: application/json;odata=nometadata
Three ways to reach the same list of items.

JSON light — control how much metadata you get back

The Accept header decides how chatty the response is. Use the lightest level your code needs — nometadata is far easier to parse, verbose is required when you write items because the entity type lives in the metadata.

3/3
Accept headerReturnsUse when
application/json;odata=nometadataBare fields, no __metadata / typeReading — simplest to parse in Parse JSON
application/json;odata=minimalmetadataFields + a little type infoDefault; a balance of size and detail
application/json;odata=verboseFields wrapped in d / __metadata / resultsLegacy samples; required by some write payloads

verbose wraps everything

With odata=verbose, a collection arrives as d.results and a single item as d. With nometadata the same data is a plain top-level value array. Match your Parse JSON schema to the header you actually send.

OData querying: $select, $filter, $expand, paging

Push shaping into the query string so the server returns only the rows and columns you need. Selecting two columns instead of forty is the single cheapest performance win, and a filter on an indexed column is what keeps a large list usable (see Large lists below).

7/7
OptionPurposeExample
$selectReturn only these columns$select=Title,Status,Amount
$filterServer-side row filter$filter=Status eq 'Open'
$expandProject fields from a lookup/person column$expand=AssignedTo&$select=AssignedTo/Title
$orderbySort (asc/desc)$orderby=Modified desc
$topPage size (first N rows)$top=100
$skiptokenContinuation cursor for the next pagePaged=TRUE&p_ID=5000
$countItem count (/items/$count)GET …/items/$count
System query options supported by the SharePoint REST service.

$skip does not work on list items

OData $skip is ignored for SharePoint list items — paging is cursor-based. Use $top for the page size and follow the odata.nextLink / __next URL (which carries $skiptoken=Paged=TRUE&p_ID=…) to get the next page. Don’t try to compute offsets.

8/8
Operator / fnMeaningExample
eq / neEquals / not equals$filter=Status eq 'Open'
gt ge lt leNumeric / date comparison$filter=Amount ge 1000
and / orCombine conditions$filter=Status eq 'Open' and Amount gt 1000
startswithPrefix match on text$filter=startswith(Title,'INV')
substringofSubstring match (note arg order)$filter=substringof('urgent',Title)
Lookup/IdFilter a lookup by its numeric Id$filter=CategoryId eq 3
Lookup/FieldFilter via an expanded lookup field$filter=Country/Title eq 'New Zealand'
Date literalQuoted ISO 8601$filter=Modified gt '2026-06-01T00:00:00Z'
$filter operators and functions.

Expand a lookup or person column

http
GET …/_api/web/lists/getbytitle('Orders')/items
  ?$select=Title,Status,AssignedTo/Title,AssignedTo/EMail,Category/Title
  &$expand=AssignedTo,Category
  &$top=100
Accept: application/json;odata=nometadata
A lookup/person column returns only an Id unless you $expand it; then $select the child fields you want.

Lookups cost you a join

Each lookup, person, or workflow-status column in a query counts toward the list view lookup threshold of 12 joins per query (queries over 8 get risky on big lists). Expand only the lookups you actually display, and prefer filtering on the raw …Id column where you can.

Creating, updating & deleting items

Reads are plain GETs. Every write is a `POST` — you signal update vs delete through the X-HTTP-Method header, not the verb. Two things trip people up: the request-digest token and the item’s entity type name.

HeaderWhenValue
X-RequestDigestEvery POST (unless using OAuth / the connector)Form digest from /_api/contextinfo
X-HTTP-MethodUpdate or deleteMERGE (partial update), PUT (replace), or DELETE
IF-MATCHUpdate / delete of a list item or fileThe item’s ETag, or "*" to force-overwrite
Content-TypePOSTs with a bodyapplication/json;odata=verbose (or nometadata)
AcceptRequests returning metadataapplication/json;odata=nometadata
Headers the SharePoint REST service expects on writes.

Get the form digest first (raw REST only)

http
POST https://contoso.sharepoint.com/sites/Ops/_api/contextinfo
Accept: application/json;odata=nometadata
// → body.FormDigestValue  →  send as X-RequestDigest on subsequent writes
A POST to contextinfo returns FormDigestValue. The connector and OAuth flows do this for you — skip it there.

Create an item

http
POST …/_api/web/lists/getbytitle('Orders')/items
Accept: application/json;odata=verbose
Content-Type: application/json;odata=verbose
X-RequestDigest: <form digest>

{
  "__metadata": { "type": "SP.Data.OrdersListItem" },
  "Title": "INV-1043",
  "Status": "Open",
  "Amount": 1299.5
}
The body’s __metadata.type is the ListItemEntityTypeFullName — usually SP.Data.<ListName>ListItem.

Find the exact entity type at runtime

Don’t guess the type name. Request ListItemEntityTypeFullName on the list (via $select) to get the precise value — spaces and odd characters in the list name get encoded (e.g. SP.Data.Order_x0020_QueueListItem). With odata=nometadata you can usually omit __metadata entirely on writes.

Update (MERGE) and delete

http
POST …/_api/web/lists/getbytitle('Orders')/items(42)
X-HTTP-Method: MERGE
IF-MATCH: "*"
Content-Type: application/json;odata=verbose
X-RequestDigest: <form digest>

{ "__metadata": { "type": "SP.Data.OrdersListItem" }, "Status": "Closed" }

POST …/_api/web/lists/getbytitle('Orders')/items(42)
X-HTTP-Method: DELETE
IF-MATCH: "*"
X-RequestDigest: <form digest>
MERGE updates only the fields you send; DELETE recycles the item. Both are POSTs.

ETags = optimistic concurrency

Pass the item’s ETag in IF-MATCH and SharePoint rejects the write with 412 Precondition Failed if someone changed the row since you read it. Use IF-MATCH: "*" only when last-write-wins is genuinely acceptable. MERGE keeps untouched fields; PUT resets anything you don’t send to its default.

Writing the awkward field types

Plain text, number, choice, and date columns take their literal value. The columns that cause support tickets are person, lookup, multi-value, and managed-metadata — each wants a particular shape.

8/8
Column typeField to setValue shape
Person/Group (single)AssignedToIdThe numeric site-user Id — e.g. 14
Person/Group (multi)AssignedToId{ "results": [14, 27] }
Lookup (single)CategoryIdThe target item’s Id — e.g. 3
Lookup (multi)CategoryId{ "results": [3, 8] }
Choice (multi)Tags{ "results": ["A", "B"] }
Yes/NoIsActivetrue / false (boolean, not string)
HyperlinkLink{ "__metadata": { "type": "SP.FieldUrlValue" }, "Url": "https://…", "Description": "Doc" }
Managed metadatahidden note fieldTermGuid + label via the taxonomy note field
How to set each tricky field type in a REST item write.

Person and lookup columns write by Id

You set a person column by posting to <InternalName>Id with the user’s site user Id (not their email or claims string). Resolve the Id first with …/_api/web/siteusers or, in Power Automate, the Resolve person action. Multi-value person, lookup, and choice columns all take a { "results": [ … ] } wrapper.

Managed metadata is two fields

A taxonomy column stores its value in a hidden note field. Setting it cleanly over REST is fiddly (TermGuid + WssId); if you can, use the Update item connector action or CSOM/PnP, which handle the note-field plumbing for you.

Large lists & the 5,000-item threshold

SharePoint Online enforces a list view threshold of 5,000 items — the most items a single database operation may touch at once. Data above 5,000 is fine and fully stored; what breaks is any *view, sort, or filter* that would have to scan more than 5,000 rows. The fix is column indexing, applied before you cross the line.

It’s the view, not the data

A 200,000-item list works perfectly — you just can’t ask the server to evaluate an unindexed column across all of it. “The number of items in this list exceeds the list view threshold (5,000)” means *narrow the query*, not *delete data*.

Index the columns you filter and sort on

An indexed column lets the server jump straight to matching rows. The golden rule: a view over the threshold works only if its first filter is on an indexed column and that filter returns ≤ 5,000 rows. Index proactively — you can add an index only while the list is under 20,000 items, and a list may hold at most 20 indexes.

11/11
Column typeIndexable?
Single line of textYes
Choice (single value)Yes
Number / CurrencyYes
Date and TimeYes
Yes/NoYes
Person or Group (single)Yes
Lookup (single)Yes
Managed Metadata (single)Yes
Multiple lines of textNo
Choice / Lookup / Person / MMD (multi-value)No
Calculated, Hyperlink/PictureNo
Which column types can be indexed.

Index Title, Created, Modified, Created By, Modified By

These five let you piece together a usable default view on almost any large list. SharePoint Online also auto-indexes some columns now, but it won’t index lists already over 20,000 items and may not pick the column your users actually filter on — plan indexes yourself rather than relying on it.

Page through a big list

http
GET …/_api/web/lists/getbytitle('BigList')/items
  ?$select=Title,Status&$filter=Status eq 'Open'   // Status indexed
  &$top=2000
Accept: application/json;odata=nometadata

// response → "odata.nextLink": "…/items?$skiptoken=Paged=TRUE&p_ID=2000&…"
// GET that nextLink for the next page; stop when it is absent.
Set a page size with $top and follow the next-link until it is empty. Filter on an indexed column first.

Power Automate Get items: turn Pagination ON

A known limitation: Get items with a Filter Query on a list over 5,000 items returns *nothing* if no match falls in the first 5,000 rows — unless you enable Pagination (action → Settings) and raise the threshold. Combine that with a filter on an indexed column. Other limits worth knowing: a list/library over 100,000 items can’t have inheritance broken at the list/folder level, and the supported ceiling is 50,000 unique permission scopes (recommended ≤ 5,000).

Throttling: 429, 503 & Retry-After

SharePoint Online throttles to protect the service. Applications get HTTP 429 (Too Many Requests) or 503 (Server Too Busy), each with a Retry-After header telling you how many seconds to wait. Honouring it is the fastest route out — throttled requests still count against your quota, so blind retries dig the hole deeper.

text
HTTP/1.1 429 Too Many Requests
Retry-After: 31
RateLimit-Limit: 1200
RateLimit-Remaining: 0
RateLimit-Reset: 31
A throttled response. Wait Retry-After seconds before the next call.
  • Honour Retry-After exactly — it is SharePoint telling you the minimum safe delay. The HTTP action’s Default retry policy already respects it.
  • Watch the preview RateLimit-Remaining header and slow down *before* you hit zero, rather than waiting for the 429.
  • Reduce concurrency and avoid request spikes — cap Apply to each parallelism; high fan-out multiplies throttling.
  • Prefer Microsoft Graph over CSOM/REST — it costs fewer resource units, so it throttles later.
  • Use delta queries with a token to scan content (1 resource unit) instead of re-reading whole lists.

Decorate your traffic

SharePoint prioritises well-decorated traffic. App-only and custom callers should send a registered App ID and a User-Agent in Microsoft’s naming convention so the service can identify (and favour) you. Undecorated traffic — no App ID, no compliant User-Agent — is throttled first.

text
User-Agent: ISV|CompanyName|AppName/Version      // building a product for others
User-Agent: NONISV|CompanyName|AppName/Version   // internal line-of-business app
User-Agent format — ISV plug-ins vs your own line-of-business apps.

Search has its own ceilings

Independently of the general limits, SharePoint search throttles app-only Sites.Read.All queries at 25 req/s, delegated user search at 10 req/s per user (aggregated across all apps), and people-search at 25 req/s org-wide. Cache results and consolidate queries rather than firing many small ones.

$batch — combine requests

The SharePoint Online REST service (and SharePoint 2016+) supports the OData $batch endpoint: pack many reads — and writes, grouped in change sets — into one HTTP call. Fewer round-trips means less latency and fewer throttling hits, since each sub-request is still costed individually but you pay one network trip.

http
POST https://contoso.sharepoint.com/sites/Ops/_api/$batch
Content-Type: multipart/mixed; boundary="batch_a1"
X-RequestDigest: <form digest>

--batch_a1
Content-Type: application/http

GET /sites/Ops/_api/web/lists/getbytitle('Orders')/items?$top=1 HTTP/1.1
Accept: application/json;odata=nometadata

--batch_a1--
POST to /_api/$batch with a multipart/mixed body; reads go loose, writes go in a changeset.

Batch is plumbing — let a library do it

Hand-rolling multipart change sets is error-prone. PnP.js, PnP PowerShell, and most SDKs expose a batch API that builds the body for you. In Power Automate, the Send an HTTP request to SharePoint action can POST a $batch body, but for bulk item writes a well-tuned Apply to each with concurrency control is usually simpler to maintain.

Permissions: inheritance, roles & unique scopes

Sites, lists, folders, and items are all securable objects. By default each inherits permissions from its parent. To give an object its own permissions you break inheritance, which copies the parent’s assignments down and then lets you add or remove them independently.

The model: role definitions + role assignments

You never grant a *right* to a user directly. A role definition (a.k.a. permission level) is a named bundle of rights; a role assignment binds a principal (user or group) to a role definition on a particular scope. Manage access by adding and removing assignments — and by managing groups, not individuals.

7/7
LevelGrants
Full ControlEverything, including managing permissions
DesignAdd/edit/delete lists; approve; customise pages
EditAdd/edit/delete list items and lists
ContributeAdd/edit/delete items; cannot manage lists
ReadView pages and items; download
View OnlyView pages/items; no download
Limited AccessSystem-assigned to reach a specific shared item; never grant by hand
Built-in permission levels (role definitions).

Break inheritance & assign a role over REST

http
// 1. Break inheritance — keep existing assignments, reset child scopes
POST …/_api/web/lists/getbytitle('Orders')/breakroleinheritance(copyRoleAssignments=true,clearSubscopes=true)
X-RequestDigest: <form digest>

// 2. Look up the role definition id for "Contribute"
GET …/_api/web/roledefinitions/getbyname('Contribute')   // → Id, e.g. 1073741827

// 3. Grant principal 18 (a SharePoint group) Contribute on the list
POST …/_api/web/lists/getbytitle('Orders')/roleassignments/addroleassignment(principalid=18,roledefid=1073741827)
X-RequestDigest: <form digest>
Stop inheriting, then bind a group (principalid) to a permission level (roledefid).
  • breakroleinheritance(copyRoleAssignments, clearSubscopes) — pass copyRoleAssignments=false to start from an empty ACL (then add yourself back), clearSubscopes=true to discard any unique permissions already set below.
  • resetroleinheritance — re-inherit from the parent and drop the object’s unique permissions.
  • Resolve principal Ids from …/_api/web/sitegroups/getbyname('…') or …/_api/web/siteusers; resolve role-definition Ids from …/roledefinitions.
  • Remove access with roleassignments/removeroleassignment(principalid=…,roledefid=…).

Unique permissions don’t scale — budget them

Every broken-inheritance scope is a unique security scope. A list supports at most 50,000 unique scopes, and performance degrades well before that — Microsoft recommends keeping it under 5,000. Item-level permissions on a big list are a classic performance trap: put items in a folder and secure the *folder*, or redesign so groups (not per-item ACLs) carry access. Lists/folders over 100,000 items can’t have inheritance broken at all.

Document libraries & files

A document library is just a list whose items are files (base template 101). So everything above applies — plus a file/folder REST surface for the binary side. Reach a folder by its server-relative URL, and a file by its server-relative URL or through its parent folder’s Files collection.

http
GET  …/_api/web/GetFolderByServerRelativeUrl('/sites/Ops/Shared Documents')/Files
GET  …/_api/web/GetFileByServerRelativeUrl('/sites/Ops/Shared Documents/q3.pdf')

POST …/_api/web/GetFolderByServerRelativeUrl('/sites/Ops/Shared Documents')/Files/Add(url='q3.pdf',overwrite=true)
X-RequestDigest: <form digest>
<binary file content in the body>
Read a folder and its files; small uploads POST the bytes to the folder’s Files/Add.

Set metadata on an uploaded file

A file is also a list item. To set its columns, reach it as a list item via ListItemAllFields and MERGE the fields — exactly like a normal item update.

http
POST …/_api/web/GetFileByServerRelativeUrl('/sites/Ops/Shared Documents/q3.pdf')/ListItemAllFields
X-HTTP-Method: MERGE
IF-MATCH: "*"
Content-Type: application/json;odata=verbose
X-RequestDigest: <form digest>

{ "__metadata": { "type": "SP.Data.Shared_x0020_DocumentsItem" },
  "Title": "Q3 report", "Department": "Finance" }
Update the uploaded file’s Title and a custom column.

Large files — upload in chunks

The simple Files/Add body is capped (~2 MB on the classic Content path; the connector’s Create file can’t chunk at all). For anything over ~10 MB, upload the file as a sequence of slices with one upload session, identified by a GUID: StartUpload on the first chunk, ContinueUpload for the middle, FinishUpload to commit. REST supports files up to 2 GB this way.

http
// create an empty file first, then:
POST …/GetFileByServerRelativeUrl('/sites/Ops/Shared Documents/big.zip')/StartUpload(uploadId=guid'4f2…')
POST …/GetFileByServerRelativeUrl('…/big.zip')/ContinueUpload(uploadId=guid'4f2…',fileOffset=10485760)
POST …/GetFileByServerRelativeUrl('…/big.zip')/FinishUpload(uploadId=guid'4f2…',fileOffset=20971520)
// each call body = that slice of bytes; the response returns the new offset
Chunked upload session — one GUID, fileOffset advances by bytes sent. Chunks must be sequential.

Chunk rules of thumb

Use ~10 MB chunks, send them in order (no parallel slices), and add a retry: an interrupted upload locks the file for ~15 minutes and unfinished files are purged after 6–24 hours. Check files out before a metadata update and check them back in — CheckOut() then CheckIn(comment='…',checkintype=0) (0 = major version).

Power Automate connector patterns

Most flows never need raw REST — the SharePoint connector covers it. The trick is filtering on the server (not pulling everything and filtering in the flow) and knowing the connector’s sharp edges.

Get items — filter on the server with a Filter Query

text
Status eq 'Open'
Status eq 'Open' and Amount gt 1000
startswith(Title, 'INV')
Country/Title eq 'New Zealand'                 // lookup column → child field
Start_x0020_Date gt '@{formatDateTime(utcNow(),'yyyy-MM-dd')}'

// Order By:  Modified desc      Top Count: 5000      Pagination: On (Settings)
Filter Query is OData. Internal names; spaces become _x0020_; wrap dates with expressions.
  • Set Top Count and turn on Pagination (Settings) for lists over 5,000 — otherwise a Filter Query that matches nothing in the first 5,000 rows returns empty.
  • Filter on an indexed column; use Limit Columns by View so you only pull the fields you need.
  • Use a trigger condition on “When an item is created or modified” to stop re-runs (e.g. only fire when a status equals a value) and avoid infinite loops when the flow writes back.
  • For person columns, get the Id with the Resolve person action before Create/Update item.
  • Cap Apply to each concurrency when each iteration calls SharePoint — it is the easiest way to throttle yourself.

When the connector can’t — Send an HTTP request to SharePoint

This standard (non-premium) action runs any SharePoint REST call as the flow’s connection, handling the digest and auth for you. Use it for permissions, folders, batch, or admin endpoints the connector doesn’t expose. Add Accept: application/json;odata=nometadata to get easily-parsed JSON light back.

text
// single object
body('Send_an_HTTP_request_to_SharePoint')['Id']

// a collection → loop body(...)['value'], then per row:
items('Apply_to_each')['Title']
Reference fields from the response body in later actions.

Connector vs generic HTTP vs Graph

Send an HTTP request to SharePoint is standard and SharePoint-only. The generic HTTP action is premium and can reach any endpoint. For cross-service or high-volume work, the HTTP with Microsoft Entra ID / Graph route throttles less. The connector’s Get item / Create item / Update item don’t support *Limit Columns by View*; only Get items does.

Field notes & gotchas

  • Index before 5,000, not after. You can only add an index while the list is under 20,000 items, and a view over threshold needs its first filter on an indexed column. Plan indexes the day you create the list.
  • Always use internal names in $filter, $select, $orderby, and Filter Query — display names lie after a rename, and spaces are _x0020_.
  • Person/lookup columns write by Id, not email or claims. Resolve the Id first; multi-value fields take a { "results": [...] } wrapper.
  • Every write is a POST with X-HTTP-Method MERGE/PUT/DELETE — and needs a request digest unless you’re using the connector or OAuth.
  • Match your Parse JSON to your Accept header — nometadata gives a flat value array; verbose nests under d.results.
  • Unique permissions are a scaling cost. Secure folders or use groups; never put per-item ACLs on a large list.
  • Honour Retry-After and decorate your traffic — undecorated, spiky callers get throttled first; prefer Graph and delta queries at volume.

Every cheat table here is searchable

Use the filter box on the tables above to jump to an operator, header, field type, or permission level fast. This guide is generated from one structured dataset, so the same reference can also back the FlowLibs MCP server for AI assistants.

← All Dev ToolsUse these in your AI assistant →
Spotted an error or have a suggestion? Let us know
FlowLibs

A curated library of production-grade Power Automate cloud flow patterns. Packaged as managed solutions, ready to import into your environment.

Library

  • Browse Cloud Flows
  • Approvals
  • Email & Notifications
  • Reporting
  • Security & Compliance

AI

  • AI Tools
  • MCP Server
  • Generate a Token

Resources

  • About
  • FAQ
  • Support
  • Status
  • Contact
  • Power Automate Docs
  • Connector Reference

© 2026 FlowLibs. All rights reserved.

  • Privacy
  • Terms
  • Refunds
  • Cookies
  • Acceptable Use
  • DMCA
Help