Files
Claude-SharepointLists/.claude/skills/sharepoint-lists/references/graph-api.md
ang3l12 65fe53710a Add person resolver: email → LookupId for writing person columns
Person/group columns store a numeric LookupId into each site's hidden User Information List, and SPO's ensureUser needs certificate auth that client secrets can't provide — so resolve by querying the hidden list directly via Graph (item id IS the LookupId). New resolvePersonByEmail() in graph.mjs (server-side EMail/UserName filter with a paged case-insensitive scan fallback), a 'person' CLI command, and a sharepoint_resolve_person MCP tool (9 tools now). Verified live against known ground truth (LookupIds 53 and 136) plus the not-found path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 19:02:25 -06:00

4.9 KiB

Graph API reference for SharePoint List items

Quick reference for the parts of the Microsoft Graph Lists API that this skill relies on. Full docs: https://learn.microsoft.com/graph/api/resources/listitem.

Internal vs. display column names

Graph reads and writes use a column's internal name, which is frequently NOT the display name you see in the UI:

  • Spaces and special characters are encoded: "Due Date" → DueDate; a column later renamed keeps its original internal name; some become OData__x005f....
  • Always discover internal names before writing: node scripts/sp.mjs columns --site "<URL>" --list "<NAME>".
  • In the columns output, the name field is the internal name; displayName is what the UI shows. Write to name.

Title is the built-in primary text column on most lists.

Field value formats by column type

When creating/updating, --fields is a JSON object of internal name → value:

Column type JSON value Example
Single line / multi-line text string {"Title":"Hello"}
Number / Currency number {"Estimate":3.5}
Yes/No (boolean) boolean {"Approved":true}
Date / DateTime ISO 8601 string (UTC) {"DueDate":"2026-07-01T00:00:00Z"}
Choice (single) the choice string {"Status":"In Progress"}
Choice (multi) needs an OData type hint (see below)
Hyperlink { "Url": "...", "Description": "..." } {"Link":{"Url":"https://x","Description":"X"}}
Lookup (single) set <Name>LookupId to the target item id {"CategoryLookupId":7}
Lookup (multi) OData type hint + array of ids (see below)
Person/Group (single) set <Name>LookupId to the user's lookup id {"AssignedToLookupId":12}

Multi-value fields need an @odata.type annotation

Graph requires you to declare the collection type alongside the value. Include both keys in the fields object:

{
  "Categories@odata.type": "Collection(Edm.String)",
  "Categories": ["Marketing", "Sales"],

  "RelatedItemsLookupId@odata.type": "Collection(Edm.Int32)",
  "RelatedItemsLookupId": [3, 9, 14]
}

Person / Lookup ids

Person and lookup columns store an integer id, not a name/email. The internal field name is usually <DisplayInternalName>LookupId. Getting the right id:

  • Lookup: the id is the target list item's id. Query the source list to find it.

  • Person: the id is the user's row id in the site's hidden User Information List. Resolve it by email with the built-in resolver — never guess:

    node scripts/sp.mjs person --site "<SITE_URL>" --email nashotay@pescoinc.biz
    # → { "lookupId": 53, "displayName": "Nashota Yazzie", "email": "..." }
    

    Then write {"ProjectManagerLookupId": 53}. Limitation: the hidden list only contains people who have accessed the site before. SharePoint's ensureUser (which force-adds someone) needs certificate-based app-only auth that client secrets can't provide — so if the resolver reports "not found", grant the person site access (or assign them once in the SharePoint UI), then retry.

If a write returns 400 invalidRequest or 400 generalException, the field name or one of these value formats is almost always the cause.

Querying items (OData)

The CLI maps flags to OData query options on /sites/{site}/lists/{list}/items:

  • --filter$filter. Filter on fields with the fields/ prefix:
    • fields/Status eq 'Open'
    • fields/Priority ge 2
    • fields/Title eq 'Exact' (text is case-sensitive in eq)
    • startswith(fields/Title,'Mig')
    • combine with and / or: fields/Status eq 'Open' and fields/Priority ge 2
  • --orderby$orderby, e.g. fields/DueDate asc or fields/Created desc.
  • --select → projects which fields come back: Title,Status,DueDate.
  • --top → page size; --all follows @odata.nextLink to return every page.

Non-indexed columns

SharePoint only allows server-side filter/sort on indexed columns unless you opt into a best-effort mode. The client automatically sends Prefer: HonorNonIndexedQueriesWarningMayFailRandomly whenever --filter or --orderby is present, which lets queries on non-indexed columns run (they may occasionally fail on very large lists — add a column index in SharePoint for heavy use). If a filter intermittently errors on a big list, that's why.

Escaping quotes

OData string literals use single quotes; a literal single quote is doubled: fields/Title eq 'O''Brien'. In the shell, wrap the whole --filter value in double quotes.

Response shape

Reads return items simplified to:

{
  "id": "42",
  "webUrl": "https://contoso.sharepoint.com/sites/.../DispForm.aspx?ID=42",
  "createdDateTime": "2026-06-01T10:00:00Z",
  "lastModifiedDateTime": "2026-06-10T12:30:00Z",
  "fields": { "Title": "...", "Status": "Open", "...": "..." }
}

id is the item id you pass to get, update, and delete.