Live training: next batches coming soon. Join the waitlist:Waitlists open:Developer Training →Developer →Copilot Studio Training →Copilot Studio →

CAML Query Builder for SharePoint Online: Build, Test and Run CAML Queries (2026)

Our sales manager asked for every order from the second half of January that still isn’t paid, newest first, with the customer and total. The orders live in a SharePoint list of 210 items.

That’s a job for a CAML query, and you don’t need a separate CAML query builder app to write one. We’ll build it piece by piece and test each piece against the real list.

In this tutorial, we’ll cover what CAML is, which caml query builder tools still work with SharePoint Online in 2026, and how to build and test a query with PnP PowerShell. Then we’ll run it through REST, PnPjs and CSOM and fix the common errors.

Here’s what we’re building: one query that returns exactly three orders, sorted by date.

Finished CAML query with ViewFields, nested And, OrderBy and RowLimit returning three unpaid SPGDemo Orders in PnP PowerShell

What Is a CAML Query in SharePoint?

CAML (Collaborative Application Markup Language) is the XML SharePoint uses to describe which list items to return. Every list view is a CAML query, and CSOM, JSOM, the SharePoint REST API and PnP PowerShell all accept one.

A query sits inside <View>: ViewFields picks columns, Where filters (Eq, Neq, Gt, Lt, Geq, Leq, Contains, BeginsWith, In, IsNull, combined with And/Or), OrderBy sorts and RowLimit caps the page size.

<View>
  <ViewFields>
    <FieldRef Name='Title' />
  </ViewFields>
  <Query>
    <Where>
      <Eq>
        <FieldRef Name='InternalName' />
        <Value Type='Text'>some value</Value>
      </Eq>
    </Where>
    <OrderBy>
      <FieldRef Name='Created' Ascending='FALSE' />
    </OrderBy>
  </Query>
  <RowLimit>100</RowLimit>
</View>

CAML Query Builder Tools: What Still Works in 2026

Microsoft has never shipped an official builder. Here’s each tool’s status from its own page (I didn’t install any):

ToolStatus in 2026
U2U Caml Query Builder (Standalone Edition)Free Windows app over CSOM, still downloadable. On-premises build (SharePoint Online only without MFA) and an online build for MFA tenants. Needs .NET Framework 4.6
U2U solution package / SharePoint CAML Query HelperLegacy SharePoint 2007-2013 tools, not maintained
CamlJs + CamlJs ConsoleJavaScript builder library and Chrome extension; last release 2.13.0 in November 2021
Lightning Tools CAML Query BuilderPart of a commercial add-on, not standalone
PnP PowerShellActively maintained, works with MFA; what we use below

With U2U, you connect to the site URL (not a page URL), pick the list, choose New Query > Query with ViewFields, add filters and copy the generated XML.

I prefer a script: write the XML, run it, check the result, add the next condition. The same XML then drops into REST, PnPjs, CSOM or an SPFx web part that displays list items.

Before You Start

You need read access to the list, PowerShell 7.4+ and PnP.PowerShell. Since 09/09/2024 it requires your own Entra ID app registration (Register-PnPEntraIDAppForInteractiveLogin creates one); the PnP PowerShell install guide covers it.

# PnP PowerShell 3.x runs on PowerShell 7.4+ and needs your own Entra ID app (client) ID
Install-Module PnP.PowerShell -Scope CurrentUser
$siteUrl  = "https://spguides.sharepoint.com/sites/SPGuides"
$clientId = "<your-app-client-id>"
Connect-PnPOnline -Url $siteUrl -ClientId $clientId -DeviceLogin -PersistLogin

We’ll query SPGDemo Orders on https://spguides.sharepoint.com/sites/SPGuides (210 orders). To build a similar list, see creating a SharePoint list and columns with PnP PowerShell. The columns we filter on:

Column (internal name)TypeSample
TitleTextSO-1106
CustomerNameTextCoho Winery
OrderTotalCurrency$479.92
OrderDateDate01/22/2026
OrderStatusChoiceNew, Processing, Shipped
PaidYes/NoNo

Step 1: Find the Internal Column Names

CAML only understands internal names, and “Order Status” might really be Order_x0020_Status. More lookup methods are in getting SharePoint list column internal names.

  1. Connect with Connect-PnPOnline.
  2. Run Get-PnPField on the list.
  3. Note each column’s InternalName and TypeAsString; the type goes into Value Type='...'.
Get-PnPField -List "SPGDemo Orders" |
    Where-Object { -not $_.Hidden -and (-not $_.ReadOnlyField -or $_.InternalName -in 'ID','Created') } |
    Select-Object Title, InternalName, TypeAsString |
    Format-Table -AutoSize
Get-PnPField output listing internal names and types of the SPGDemo Orders columns with OrderDate, OrderStatus and Paid highlighted

Checkpoint: Paid is Boolean, OrderDate is DateTime and OrderStatus is Choice. Internal names are fixed at creation; see tips for customizing list fields with PnP PowerShell.

Step 2: Write the First Condition

Start with one condition and prove it works. “Not paid” means Paid equals 0:

$caml = @"
<View>
  <Query>
    <Where>
      <Eq>
        <FieldRef Name='Paid' />
        <Value Type='Boolean'>0</Value>
      </Eq>
    </Where>
  </Query>
</View>
"@

$items = Get-PnPListItem -List "SPGDemo Orders" -Query $caml
"Unpaid orders: $($items.Count)"
$items | ForEach-Object { New-Object psobject -Property $_.FieldValues } |
    Format-Table ID, Title, CustomerName, OrderTotal, OrderDate, OrderStatus, Paid -AutoSize
CAML Eq query on the Paid Boolean column returning four unpaid orders in PnP PowerShell

Checkpoint: Four orders come back: IDs 3, 7, 8 and 10. The 201 items with no Paid value don’t match Eq 0.

Yes/No columns trip up a lot of forum answers, so I ran the same filter three ways:

$list = "SPGDemo Orders"
$tests = [ordered]@{
    "Type='Boolean' 1"     = "<Value Type='Boolean'>1</Value>"
    "Type='Boolean' TRUE"  = "<Value Type='Boolean'>TRUE</Value>"
    "Type='Bool' True"     = "<Value Type='Bool'>True</Value>"
}
foreach ($t in $tests.GetEnumerator()) {
    $caml = "<View><Query><Where><Eq><FieldRef Name='Paid' />$($t.Value)</Eq></Where></Query></View>"
    $ids = (Get-PnPListItem -List $list -Query $caml).Id -join ', '
    "{0,-22} -> IDs {1}" -f $t.Key, $ids
}
PowerShell test showing CAML Boolean value TRUE returns the unpaid orders while 1 and Bool True return the paid ones

Type='Boolean' with TRUE silently returned the unpaid orders. Use 1 or 0, or Type='Bool' with True.

Step 3: Combine Conditions With Nested And

Now add the date range 01/15/2026 to 01/31/2026. That makes three conditions, and my first try put them all in one <And>:

<!-- Wrong: three conditions inside ONE <And> -->
<Where>
  <And>
    <Eq><FieldRef Name='Paid' /><Value Type='Boolean'>0</Value></Eq>
    <Geq><FieldRef Name='OrderDate' /><Value Type='DateTime'>2026-01-15</Value></Geq>
    <Leq><FieldRef Name='OrderDate' /><Value Type='DateTime'>2026-01-31</Value></Leq>
  </And>
</Where>
Three conditions inside one CAML And element failing with Cannot complete this action Please try again

SharePoint answers “Cannot complete this action. Please try again.” Each <And> or <Or> takes exactly two children, so nest them:

  1. Keep Paid as the first child of the outer <And>.
  2. Wrap both date conditions in a second <And>.
  3. Write dates as yyyy-MM-dd. Without IncludeTimeValue='TRUE', the time part is ignored.
<View><Query><Where>
  <And>
    <Eq><FieldRef Name='Paid' /><Value Type='Boolean'>0</Value></Eq>
    <And>
      <Geq><FieldRef Name='OrderDate' /><Value Type='DateTime'>2026-01-15</Value></Geq>
      <Leq><FieldRef Name='OrderDate' /><Value Type='DateTime'>2026-01-31</Value></Leq>
    </And>
  </And>
</Where></Query></View>
Nested CAML And with a Geq and Leq date range returning three unpaid orders from the second half of January 2026

Checkpoint: Three orders remain: SO-1105, SO-1106 and SO-1108. SO-1101 (01/05/2026) dropped out. For a fourth condition, nest again: And(A, And(B, And(C, D))).

Step 4: Sort, Pick Columns and Limit Rows

  1. Add <OrderBy> with OrderDate and Ascending='FALSE'.
  2. Add <ViewFields> for the columns you need (-Fields can’t be combined with -Query).
  3. Add <RowLimit>10</RowLimit>.
<View>
  <ViewFields>
    <FieldRef Name='Title' /><FieldRef Name='CustomerName' />
    <FieldRef Name='OrderTotal' /><FieldRef Name='OrderDate' />
  </ViewFields>
  <Query>
    <Where>
      <And>
        <Eq><FieldRef Name='Paid' /><Value Type='Boolean'>0</Value></Eq>
        <And>
          <Geq><FieldRef Name='OrderDate' /><Value Type='DateTime'>2026-01-15</Value></Geq>
          <Leq><FieldRef Name='OrderDate' /><Value Type='DateTime'>2026-01-31</Value></Leq>
        </And>
      </And>
    </Where>
    <OrderBy><FieldRef Name='OrderDate' Ascending='FALSE' /></OrderBy>
  </Query>
  <RowLimit>10</RowLimit>
</View>
Get-PnPListItem -List "SPGDemo Orders" -Query $caml |
    ForEach-Object { New-Object psobject -Property $_.FieldValues } |
    Format-Table ID, Title, CustomerName, OrderTotal, OrderDate -AutoSize
ID Title   CustomerName         OrderTotal OrderDate
-- -----   ------------         ---------- ---------
10 SO-1108 Proseware Inc.          299.950 1/28/2026 12:00:00 AM
 8 SO-1106 Coho Winery             479.920 1/22/2026 12:00:00 AM
 7 SO-1105 Blue Yonder Airlines    189.000 1/19/2026 12:00:00 AM

Checkpoint: Orders 10, 8 and 7 come back newest first: the result from the top of this post.

Step 5: Run the CAML Query With the REST API (GetItems)

REST’s $filter can’t do Membership or In, but CAML can. POST the XML to the list’s GetItems endpoint in query.ViewXml; a browser session also needs X-RequestDigest. The created date range REST example uses the same endpoint style from Power Automate.

POST https://spguides.sharepoint.com/sites/SPGuides/_api/web/lists/getbytitle('SPGDemo Orders')/GetItems?$select=ID,Title,CustomerName,OrderTotal
Accept: application/json;odata=nometadata
Content-Type: application/json;odata=nometadata
X-RequestDigest: <form digest value from /_api/contextinfo>

{
  "query": {
    "ViewXml": "<View><Query><Where><Eq><FieldRef Name='Paid' /><Value Type='Boolean'>0</Value></Eq></Where></Query><RowLimit>10</RowLimit></View>"
  }
}

I sent the Step 4 query with Invoke-PnPSPRestMethod, which signs the request for you:

$url = "/_api/web/lists/getbytitle('SPGDemo Orders')/GetItems?`$select=ID,Title,CustomerName,OrderTotal"
$r = Invoke-PnPSPRestMethod -Method Post -Url $url `
    -Content @{ query = @{ ViewXml = $caml } } -ContentType "application/json;odata=nometadata"

$r.value | Format-Table ID, Title, CustomerName, OrderTotal -AutoSize
Invoke-PnPSPRestMethod posting the CAML ViewXml to the SharePoint REST GetItems endpoint and returning three orders
{
  "value": [
    { "ID": 10, "Title": "SO-1108", "CustomerName": "Proseware Inc.", "OrderTotal": 299.95 },
    { "ID": 8,  "Title": "SO-1106", "CustomerName": "Coho Winery", "OrderTotal": 479.92 },
    { "ID": 7,  "Title": "SO-1105", "CustomerName": "Blue Yonder Airlines", "OrderTotal": 189 }
  ]
}

Step 6: Use the Same Query in PnPjs and CSOM

PnPjs wraps GetItems as getItemsByCAMLQuery. For a full web part, see filtering SharePoint list items in SPFx:

import { spfi, SPFx } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import { ICamlQuery } from "@pnp/sp/lists";

const sp = spfi().using(SPFx(this.context));

const caml: ICamlQuery = {
  ViewXml: `<View>
    <ViewFields><FieldRef Name='Title' /><FieldRef Name='CustomerName' /><FieldRef Name='OrderTotal' /></ViewFields>
    <Query><Where><Eq><FieldRef Name='Paid' /><Value Type='Boolean'>0</Value></Eq></Where>
      <OrderBy><FieldRef Name='OrderDate' Ascending='FALSE' /></OrderBy></Query>
    <RowLimit>10</RowLimit>
  </View>`
};

const orders = await sp.web.lists.getByTitle("SPGDemo Orders").getItemsByCAMLQuery(caml);
console.log(orders.map(o => `${o.Title} ${o.CustomerName} ${o.OrderTotal}`));

In CSOM the XML goes into a CamlQuery. I ran it through the PnP connection’s client context, the same class a C# app uses (see the CSOM in SharePoint Online guide):

$ctx  = Get-PnPContext
$list = $ctx.Web.Lists.GetByTitle("SPGDemo Orders")
$query = New-Object Microsoft.SharePoint.Client.CamlQuery
$query.ViewXml = $caml
$items = $list.GetItems($query)
$ctx.Load($items)
$ctx.ExecuteQuery()

foreach ($i in $items) { "{0}  {1,-14} {2,-20} {3,10:C2}" -f $i.Id, $i["Title"], $i["CustomerName"], $i["OrderTotal"] }
CSOM CamlQuery with ViewXml run through the PnP PowerShell client context returning the same three orders

The C# version uses an access token; the old SharePointOnlineCredentials password sample doesn’t work with MFA:

using Microsoft.SharePoint.Client;

// ctx = a ClientContext with a modern (Entra ID) access token, e.g. from PnP.Framework's AuthenticationManager
List list = ctx.Web.Lists.GetByTitle("SPGDemo Orders");
var query = new CamlQuery { ViewXml = viewXml };   // the full <View>...</View> from Step 4

ListItemCollection items = list.GetItems(query);
ctx.Load(items);
await ctx.ExecuteQueryAsync();

foreach (ListItem item in items)
{
    Console.WriteLine($"{item.Id} {item["Title"]} {item["CustomerName"]} {item["OrderTotal"]:C2}");
}

Test the Query Against the List

PowerShell, REST and CSOM all returned IDs 10, 8 and 7, and the counts add up: 210 orders, 4 unpaid, 3 in the date range. Test the edges too: 2026-15-01 instead of 2026-01-15 returned 0 items with no error.

More examples I ran against the same list. Status is New or Processing (205 items):

<Where>
  <In>
    <FieldRef Name='OrderStatus' />
    <Values>
      <Value Type='Choice'>New</Value>
      <Value Type='Choice'>Processing</Value>
    </Values>
  </In>
</Where>

Customer contains “Inc” (3 items):

<Where><Contains><FieldRef Name='CustomerName' /><Value Type='Text'>Inc</Value></Contains></Where>

Lookup by product ID (53 items; IDs survive a product rename, text matches don’t):

<Where><Eq><FieldRef Name='Product' LookupId='TRUE' /><Value Type='Lookup'>3</Value></Eq></Where>

Order date on or before today (9 items):

<Where><Leq><FieldRef Name='OrderDate' /><Value Type='DateTime'><Today /></Value></Leq></Where>

Sales rep is me or one of my groups (0 for me):

<Where>
  <Or>
    <Membership Type='CurrentUserGroups'><FieldRef Name='SalesRep' /></Membership>
    <Eq><FieldRef Name='SalesRep' /><Value Type='Integer'><UserID Type='Integer' /></Value></Eq>
  </Or>
</Where>

One item by ID (1 item) and empty Paid values (201 items):

<Where><Eq><FieldRef Name='ID' /><Value Type='Counter'>8</Value></Eq></Where>
<Where><IsNull><FieldRef Name='Paid' /></IsNull></Where>

Make It Production-Ready

  • SharePoint Online’s list view threshold is 5,000 items. Filter on an indexed column first.
  • Set RowLimit and page with ListItemCollectionPosition or -PageSize. Power Automate does the same with REST API pagination.
  • Keep lookup and person columns to 12 or fewer per query.
  • Add Scope='RecursiveAll' to <View> when items sit in folders.
  • Escape user input (&, <) before it goes into the XML, and test with volume using more than 5,000 items added with PnP PowerShell.

This paging loop returned 100 + 100 + 10 items:

$query = New-Object Microsoft.SharePoint.Client.CamlQuery
$query.ViewXml = "<View><RowLimit>100</RowLimit></View>"
$total = 0
do {
    $items = $list.GetItems($query)
    $ctx.Load($items)
    $ctx.ExecuteQuery()
    $total += $items.Count
    $query.ListItemCollectionPosition = $items.ListItemCollectionPosition   # null after the last page
} while ($query.ListItemCollectionPosition)
"Total: $total"   # 100 + 100 + 10 = 210 on SPGDemo Orders

Troubleshooting

ProblemFix
Cannot complete this action. Please try again.Three children in one And/Or, or broken XML. Nest two at a time.
One or more field types are not installed properlyDisplay name used; use the internal name.
Filter ignored, every item returnedCSOM ViewXml must start with <View><Query>; a bare <Query> returned all 210.
Yes/No filter returns the opposite itemsWith Type=’Boolean’ use 1 or 0, not TRUE.
Date range returns nothingUse yyyy-MM-dd.
Choice values 1-13 sort as 1, 10, 11, 2Choices sort as text; use a Number column.
Need “not contains”CAML has none; use Neq for exact values or filter Contains results in code.

Threshold errors from code are covered in this list view threshold fix.

Where to Take It Next

Power Automate’s Get items doesn’t take CAML, so translate the conditions with the Power Automate OData filter query guide. You can also create indexed columns with Power Automate before the list grows.

Classic pages can run the same CamlQuery in the JSOM guide with examples, and CRUD operations with PnP PowerShell pairs well with these queries.

For more projects, browse the SharePoint tutorials.

A few more SharePoint developer posts to keep handy:

Frequently Asked Questions

What is a CAML query builder?

A tool that writes CAML XML from a visual form. Microsoft doesn’t ship one; the best-known is the free U2U Caml Query Builder, and many developers now test CAML directly with PnP PowerShell.

Does the U2U CAML Query Builder work with SharePoint Online?

Yes. U2U’s software page offers an on-premises build that works with SharePoint Online without MFA and an online build for MFA tenants. Both need .NET Framework 4.6.

Is there an online CAML query builder?

Not from Microsoft. The CamlJs Console Chrome extension previews queries against a real list, but its last release is from 2021.

Why does my CAML query say Cannot complete this action?

Usually an And or Or has more than two children, or the XML is malformed. Microsoft’s And element reference says each And takes only two conditions.

Can I use a CAML query with the SharePoint REST API?

Yes. POST to /_api/web/lists/getbytitle(‘List’)/GetItems with {“query”:{“ViewXml”:”<View>…</View>”}}. PnPjs wraps it as getItemsByCAMLQuery.

Leave a Comment