From 4f19d8447c7364e2c4fe370cb5fe5d2b7d0a2072 Mon Sep 17 00:00:00 2001
From: waleed
Date: Thu, 2 Jul 2026 00:18:49 -0700
Subject: [PATCH 1/6] feat(sharepoint): validate against Graph API and add
delete/update/download tools
- Fix bugs found validating existing tools against live Graph docs: malformed
nested $expand syntax in get_list, reversed site-resolution precedence in
create_list, unsanitized field fallback in add_list_items, missing URL
encoding in read_page, and an incorrect root/serverRelativeUrl field shape
- Fix V1 block gaps vs V2: upload_file required flag, missing required on
pageName/listDisplayName, wrong site-picker mimeType, dead subBlock refs
- Add 8 new tools within already-granted scopes for CRUD completion:
delete_list_item, get_list_item, delete_page, update_page, publish_page,
download_file, delete_file, get_drive_item
- Add opt-in stripAuthOnRedirect option to secureFetchWithPinnedIP so the
SharePoint file-download route doesn't resend the bearer token to the
preauthenticated redirect target (default off, no behavior change elsewhere)
- Dedupe read-only list-item field sanitization into a shared utils helper
---
.../tools/sharepoint/download-file/route.ts | 176 +++++++++++
apps/sim/blocks/blocks/sharepoint.ts | 297 +++++++++++++++---
apps/sim/lib/api/contracts/tools/microsoft.ts | 15 +
.../core/security/input-validation.server.ts | 11 +-
apps/sim/tools/registry.ts | 16 +
apps/sim/tools/sharepoint/add_list_items.ts | 105 ++-----
apps/sim/tools/sharepoint/create_list.ts | 2 +-
apps/sim/tools/sharepoint/delete_file.ts | 66 ++++
apps/sim/tools/sharepoint/delete_list_item.ts | 88 ++++++
apps/sim/tools/sharepoint/delete_page.ts | 72 +++++
apps/sim/tools/sharepoint/download_file.ts | 59 ++++
apps/sim/tools/sharepoint/get_drive_item.ts | 106 +++++++
apps/sim/tools/sharepoint/get_list.ts | 81 ++---
apps/sim/tools/sharepoint/get_list_item.ts | 96 ++++++
apps/sim/tools/sharepoint/index.ts | 18 +-
apps/sim/tools/sharepoint/list_sites.ts | 6 +-
apps/sim/tools/sharepoint/publish_page.ts | 72 +++++
apps/sim/tools/sharepoint/read_page.ts | 14 +-
apps/sim/tools/sharepoint/types.ts | 132 +++++---
apps/sim/tools/sharepoint/update_list.ts | 53 +---
apps/sim/tools/sharepoint/update_page.ts | 168 ++++++++++
apps/sim/tools/sharepoint/utils.ts | 51 +++
scripts/check-api-validation-contracts.ts | 4 +-
23 files changed, 1452 insertions(+), 256 deletions(-)
create mode 100644 apps/sim/app/api/tools/sharepoint/download-file/route.ts
create mode 100644 apps/sim/tools/sharepoint/delete_file.ts
create mode 100644 apps/sim/tools/sharepoint/delete_list_item.ts
create mode 100644 apps/sim/tools/sharepoint/delete_page.ts
create mode 100644 apps/sim/tools/sharepoint/download_file.ts
create mode 100644 apps/sim/tools/sharepoint/get_drive_item.ts
create mode 100644 apps/sim/tools/sharepoint/get_list_item.ts
create mode 100644 apps/sim/tools/sharepoint/publish_page.ts
create mode 100644 apps/sim/tools/sharepoint/update_page.ts
diff --git a/apps/sim/app/api/tools/sharepoint/download-file/route.ts b/apps/sim/app/api/tools/sharepoint/download-file/route.ts
new file mode 100644
index 00000000000..6f3bb947770
--- /dev/null
+++ b/apps/sim/app/api/tools/sharepoint/download-file/route.ts
@@ -0,0 +1,176 @@
+import { createLogger } from '@sim/logger'
+import { getErrorMessage } from '@sim/utils/errors'
+import { type NextRequest, NextResponse } from 'next/server'
+import { sharepointDownloadFileContract } from '@/lib/api/contracts/tools/microsoft'
+import { parseRequest } from '@/lib/api/server'
+import { checkInternalAuth } from '@/lib/auth/hybrid'
+import {
+ secureFetchWithPinnedIP,
+ validateUrlWithDNS,
+} from '@/lib/core/security/input-validation.server'
+import { generateRequestId } from '@/lib/core/utils/request'
+import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
+
+export const dynamic = 'force-dynamic'
+
+/** Microsoft Graph API error response structure */
+interface GraphApiError {
+ error?: {
+ code?: string
+ message?: string
+ }
+}
+
+/** Microsoft Graph API drive item metadata response */
+interface DriveItemMetadata {
+ id?: string
+ name?: string
+ folder?: Record
+ file?: {
+ mimeType?: string
+ }
+}
+
+const logger = createLogger('SharepointDownloadFileAPI')
+
+export const POST = withRouteHandler(async (request: NextRequest) => {
+ const requestId = generateRequestId()
+
+ try {
+ const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
+
+ if (!authResult.success) {
+ logger.warn(`[${requestId}] Unauthorized SharePoint download attempt: ${authResult.error}`)
+ return NextResponse.json(
+ {
+ success: false,
+ error: authResult.error || 'Authentication required',
+ },
+ { status: 401 }
+ )
+ }
+
+ const parsed = await parseRequest(sharepointDownloadFileContract, request, {})
+ if (!parsed.success) return parsed.response
+ const { accessToken, driveId, itemId, fileName } = parsed.data.body
+ const authHeader = `Bearer ${accessToken}`
+
+ logger.info(`[${requestId}] Getting file metadata from SharePoint`, { driveId, itemId })
+
+ const metadataUrl = `https://graph.microsoft.com/v1.0/drives/${encodeURIComponent(driveId)}/items/${encodeURIComponent(itemId)}`
+ const metadataUrlValidation = await validateUrlWithDNS(metadataUrl, 'metadataUrl')
+ if (!metadataUrlValidation.isValid) {
+ return NextResponse.json(
+ { success: false, error: metadataUrlValidation.error },
+ { status: 400 }
+ )
+ }
+
+ const metadataResponse = await secureFetchWithPinnedIP(
+ metadataUrl,
+ metadataUrlValidation.resolvedIP!,
+ {
+ headers: { Authorization: authHeader },
+ }
+ )
+
+ if (!metadataResponse.ok) {
+ const errorDetails = (await metadataResponse.json().catch(() => ({}))) as GraphApiError
+ logger.error(`[${requestId}] Failed to get file metadata`, {
+ status: metadataResponse.status,
+ error: errorDetails,
+ })
+ return NextResponse.json(
+ { success: false, error: errorDetails.error?.message || 'Failed to get file metadata' },
+ { status: 400 }
+ )
+ }
+
+ const metadata = (await metadataResponse.json()) as DriveItemMetadata
+
+ if (metadata.folder && !metadata.file) {
+ logger.error(`[${requestId}] Attempted to download a folder`, {
+ itemId: metadata.id,
+ itemName: metadata.name,
+ })
+ return NextResponse.json(
+ {
+ success: false,
+ error: `Cannot download folder "${metadata.name}". Please select a file instead.`,
+ },
+ { status: 400 }
+ )
+ }
+
+ const mimeType = metadata.file?.mimeType || 'application/octet-stream'
+
+ logger.info(`[${requestId}] Downloading file from SharePoint`, { driveId, itemId, mimeType })
+
+ const downloadUrl = `https://graph.microsoft.com/v1.0/drives/${encodeURIComponent(driveId)}/items/${encodeURIComponent(itemId)}/content`
+ const downloadUrlValidation = await validateUrlWithDNS(downloadUrl, 'downloadUrl')
+ if (!downloadUrlValidation.isValid) {
+ return NextResponse.json(
+ { success: false, error: downloadUrlValidation.error },
+ { status: 400 }
+ )
+ }
+
+ const downloadResponse = await secureFetchWithPinnedIP(
+ downloadUrl,
+ downloadUrlValidation.resolvedIP!,
+ {
+ headers: { Authorization: authHeader },
+ // The content endpoint 302s to a preauthenticated URL on a different origin that needs no auth.
+ stripAuthOnRedirect: true,
+ }
+ )
+
+ if (!downloadResponse.ok) {
+ const downloadError = (await downloadResponse.json().catch(() => ({}))) as GraphApiError
+ logger.error(`[${requestId}] Failed to download file`, {
+ status: downloadResponse.status,
+ error: downloadError,
+ })
+ return NextResponse.json(
+ { success: false, error: downloadError.error?.message || 'Failed to download file' },
+ { status: 400 }
+ )
+ }
+
+ const arrayBuffer = await downloadResponse.arrayBuffer()
+ const fileBuffer = Buffer.from(arrayBuffer)
+
+ const resolvedName = fileName || metadata.name || 'download'
+
+ logger.info(`[${requestId}] File downloaded successfully`, {
+ driveId,
+ itemId,
+ name: resolvedName,
+ size: fileBuffer.length,
+ mimeType,
+ })
+
+ const base64Data = fileBuffer.toString('base64')
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ file: {
+ name: resolvedName,
+ mimeType,
+ data: base64Data,
+ size: fileBuffer.length,
+ },
+ },
+ })
+ } catch (error) {
+ logger.error(`[${requestId}] Error downloading SharePoint file:`, error)
+ return NextResponse.json(
+ {
+ success: false,
+ error: getErrorMessage(error, 'Unknown error occurred'),
+ },
+ { status: 500 }
+ )
+ }
+})
diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts
index 489b45ebcee..84b1ba230fd 100644
--- a/apps/sim/blocks/blocks/sharepoint.ts
+++ b/apps/sim/blocks/blocks/sharepoint.ts
@@ -30,12 +30,20 @@ export const SharepointBlock: BlockConfig = {
options: [
{ label: 'Create Page', id: 'create_page' },
{ label: 'Read Page', id: 'read_page' },
+ { label: 'Update Page', id: 'update_page' },
+ { label: 'Publish Page', id: 'publish_page' },
+ { label: 'Delete Page', id: 'delete_page' },
{ label: 'List Sites', id: 'list_sites' },
{ label: 'Create List', id: 'create_list' },
{ label: 'Read List', id: 'read_list' },
{ label: 'Update List', id: 'update_list' },
{ label: 'Add List Items', id: 'add_list_items' },
+ { label: 'Get List Item', id: 'get_list_item' },
+ { label: 'Delete List Item', id: 'delete_list_item' },
{ label: 'Upload File', id: 'upload_file' },
+ { label: 'Download File', id: 'download_file' },
+ { label: 'Get Drive Item', id: 'get_drive_item' },
+ { label: 'Delete File', id: 'delete_file' },
],
},
{
@@ -65,7 +73,7 @@ export const SharepointBlock: BlockConfig = {
serviceId: 'sharepoint',
selectorKey: 'sharepoint.sites',
requiredScopes: getScopesForService('sharepoint'),
- mimeType: 'application/vnd.microsoft.graph.folder',
+ mimeType: 'application/vnd.microsoft.graph.site',
placeholder: 'Select a site',
dependsOn: ['credential'],
mode: 'basic',
@@ -74,11 +82,16 @@ export const SharepointBlock: BlockConfig = {
value: [
'create_page',
'read_page',
+ 'update_page',
+ 'publish_page',
+ 'delete_page',
'list_sites',
'create_list',
'read_list',
'update_list',
'add_list_items',
+ 'get_list_item',
+ 'delete_list_item',
'upload_file',
],
},
@@ -90,14 +103,37 @@ export const SharepointBlock: BlockConfig = {
type: 'short-input',
placeholder: 'Name of the page',
condition: { field: 'operation', value: ['create_page', 'read_page'] },
+ required: { field: 'operation', value: 'create_page' },
+ },
+
+ {
+ id: 'pageTitle',
+ title: 'Page Title',
+ type: 'short-input',
+ placeholder: 'Optional title (defaults to page name)',
+ condition: { field: 'operation', value: ['create_page', 'update_page'] },
+ mode: 'advanced',
+ },
+
+ {
+ id: 'pageContent',
+ title: 'Page Content',
+ type: 'long-input',
+ placeholder: 'Optional text content for the page',
+ condition: { field: 'operation', value: ['create_page', 'update_page'] },
+ mode: 'advanced',
},
{
id: 'pageId',
title: 'Page ID',
type: 'short-input',
- placeholder: 'Page ID (alternative to page name)',
- condition: { field: 'operation', value: 'read_page' },
+ placeholder: 'Page ID',
+ condition: {
+ field: 'operation',
+ value: ['read_page', 'update_page', 'publish_page', 'delete_page'],
+ },
+ required: { field: 'operation', value: ['update_page', 'publish_page', 'delete_page'] },
mode: 'advanced',
},
@@ -111,7 +147,14 @@ export const SharepointBlock: BlockConfig = {
placeholder: 'Select a list',
dependsOn: ['credential', 'siteSelector'],
mode: 'basic',
- condition: { field: 'operation', value: ['read_list', 'update_list', 'add_list_items'] },
+ condition: {
+ field: 'operation',
+ value: ['read_list', 'update_list', 'add_list_items', 'get_list_item', 'delete_list_item'],
+ },
+ required: {
+ field: 'operation',
+ value: ['update_list', 'add_list_items', 'get_list_item', 'delete_list_item'],
+ },
},
{
id: 'listId',
@@ -120,7 +163,14 @@ export const SharepointBlock: BlockConfig = {
canonicalParamId: 'listId',
placeholder: 'Enter list ID (GUID). Required for Update; optional for Read.',
mode: 'advanced',
- condition: { field: 'operation', value: ['read_list', 'update_list', 'add_list_items'] },
+ condition: {
+ field: 'operation',
+ value: ['read_list', 'update_list', 'add_list_items', 'get_list_item', 'delete_list_item'],
+ },
+ required: {
+ field: 'operation',
+ value: ['update_list', 'add_list_items', 'get_list_item', 'delete_list_item'],
+ },
},
{
@@ -129,7 +179,11 @@ export const SharepointBlock: BlockConfig = {
type: 'short-input',
placeholder: 'Enter item ID',
canonicalParamId: 'itemId',
- condition: { field: 'operation', value: ['update_list'] },
+ condition: {
+ field: 'operation',
+ value: ['update_list', 'get_list_item', 'delete_list_item'],
+ },
+ required: { field: 'operation', value: ['get_list_item', 'delete_list_item'] },
},
{
@@ -138,6 +192,7 @@ export const SharepointBlock: BlockConfig = {
type: 'short-input',
placeholder: 'Name of the list',
condition: { field: 'operation', value: 'create_list' },
+ required: { field: 'operation', value: 'create_list' },
},
{
@@ -270,11 +325,16 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
value: [
'create_page',
'read_page',
+ 'update_page',
+ 'publish_page',
+ 'delete_page',
'list_sites',
'create_list',
'read_list',
'update_list',
'add_list_items',
+ 'get_list_item',
+ 'delete_list_item',
'upload_file',
],
},
@@ -330,16 +390,29 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
},
},
- // Upload File operation fields
+ // Upload / Download / Delete File / Get Drive Item operation fields
{
id: 'driveId',
title: 'Document Library ID',
type: 'short-input',
placeholder: 'Enter document library (drive) ID',
canonicalParamId: 'driveId',
- condition: { field: 'operation', value: 'upload_file' },
+ condition: {
+ field: 'operation',
+ value: ['upload_file', 'download_file', 'delete_file', 'get_drive_item'],
+ },
+ required: { field: 'operation', value: ['download_file', 'delete_file', 'get_drive_item'] },
mode: 'advanced',
},
+ {
+ id: 'driveItemId',
+ title: 'File ID',
+ type: 'short-input',
+ placeholder: 'Enter the file (drive item) ID',
+ canonicalParamId: 'driveItemId',
+ condition: { field: 'operation', value: ['download_file', 'delete_file', 'get_drive_item'] },
+ required: { field: 'operation', value: ['download_file', 'delete_file', 'get_drive_item'] },
+ },
{
id: 'folderPath',
title: 'Folder Path',
@@ -352,8 +425,8 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
id: 'fileName',
title: 'File Name',
type: 'short-input',
- placeholder: 'Optional: override uploaded file name',
- condition: { field: 'operation', value: 'upload_file' },
+ placeholder: 'Optional: override uploaded/downloaded file name',
+ condition: { field: 'operation', value: ['upload_file', 'download_file'] },
mode: 'advanced',
required: false,
},
@@ -367,7 +440,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
condition: { field: 'operation', value: 'upload_file' },
mode: 'basic',
multiple: true,
- required: false,
+ required: true,
},
// Variable reference (advanced mode)
{
@@ -378,19 +451,27 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
placeholder: 'Reference files from previous blocks',
condition: { field: 'operation', value: 'upload_file' },
mode: 'advanced',
- required: false,
+ required: true,
},
],
tools: {
access: [
'sharepoint_create_page',
'sharepoint_read_page',
+ 'sharepoint_update_page',
+ 'sharepoint_publish_page',
+ 'sharepoint_delete_page',
'sharepoint_list_sites',
'sharepoint_create_list',
'sharepoint_get_list',
'sharepoint_update_list',
'sharepoint_add_list_items',
+ 'sharepoint_get_list_item',
+ 'sharepoint_delete_list_item',
'sharepoint_upload_file',
+ 'sharepoint_download_file',
+ 'sharepoint_get_drive_item',
+ 'sharepoint_delete_file',
],
config: {
tool: (params) => {
@@ -399,6 +480,12 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
return 'sharepoint_create_page'
case 'read_page':
return 'sharepoint_read_page'
+ case 'update_page':
+ return 'sharepoint_update_page'
+ case 'publish_page':
+ return 'sharepoint_publish_page'
+ case 'delete_page':
+ return 'sharepoint_delete_page'
case 'list_sites':
return 'sharepoint_list_sites'
case 'create_list':
@@ -409,8 +496,18 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
return 'sharepoint_update_list'
case 'add_list_items':
return 'sharepoint_add_list_items'
+ case 'get_list_item':
+ return 'sharepoint_get_list_item'
+ case 'delete_list_item':
+ return 'sharepoint_delete_list_item'
case 'upload_file':
return 'sharepoint_upload_file'
+ case 'download_file':
+ return 'sharepoint_download_file'
+ case 'get_drive_item':
+ return 'sharepoint_get_drive_item'
+ case 'delete_file':
+ return 'sharepoint_delete_file'
default:
throw new Error(`Invalid Sharepoint operation: ${params.operation}`)
}
@@ -428,6 +525,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
includeItems,
files, // canonical param from uploadFiles (basic) or files (advanced)
driveId, // canonical param from driveId
+ driveItemId, // canonical param from driveItemId
columnDefinitions,
listId,
...others
@@ -458,19 +556,16 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
}
if (others.operation === 'update_list' || others.operation === 'add_list_items') {
- try {
- logger.info('SharepointBlock list item param check', {
- siteId: effectiveSiteId || undefined,
- listId: listId,
- listTitle: (others as any)?.listTitle,
- itemId: sanitizedItemId,
- hasItemFields: !!parsedItemFields && typeof parsedItemFields === 'object',
- itemFieldKeys:
- parsedItemFields && typeof parsedItemFields === 'object'
- ? Object.keys(parsedItemFields)
- : [],
- })
- } catch {}
+ logger.info('SharepointBlock list item param check', {
+ siteId: effectiveSiteId || undefined,
+ listId: listId,
+ itemId: sanitizedItemId,
+ hasItemFields: !!parsedItemFields && typeof parsedItemFields === 'object',
+ itemFieldKeys:
+ parsedItemFields && typeof parsedItemFields === 'object'
+ ? Object.keys(parsedItemFields)
+ : [],
+ })
}
// Handle file upload files parameter using canonical param
@@ -478,11 +573,10 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
const baseParams: Record = {
oauthCredential,
siteId: effectiveSiteId || undefined,
- pageSize: others.pageSize ? Number.parseInt(others.pageSize as string, 10) : undefined,
- mimeType: mimeType,
...others,
...(listId ? { listId } : {}),
...(driveId ? { driveId } : {}),
+ ...(driveItemId ? { driveItemId: String(driveItemId).trim() } : {}),
itemId: sanitizedItemId,
listItemFields: parsedItemFields,
includeColumns: coerceBoolean(includeColumns),
@@ -511,14 +605,13 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
description: 'Column definitions for list creation (JSON array)',
},
pageTitle: { type: 'string', description: 'Page title' },
+ pageContent: { type: 'string', description: 'Page text content' },
pageId: { type: 'string', description: 'Page ID' },
siteId: { type: 'string', description: 'Site ID' },
- pageSize: { type: 'number', description: 'Results per page' },
listDisplayName: { type: 'string', description: 'List display name' },
listDescription: { type: 'string', description: 'List description' },
listTemplate: { type: 'string', description: 'List template' },
listId: { type: 'string', description: 'List ID' },
- listTitle: { type: 'string', description: 'List title' },
includeColumns: { type: 'boolean', description: 'Include columns in response' },
includeItems: { type: 'boolean', description: 'Include items in response' },
itemId: { type: 'string', description: 'List item ID (canonical param)' },
@@ -527,6 +620,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
type: 'string',
description: 'Document library (drive) ID',
},
+ driveItemId: { type: 'string', description: 'File (drive item) ID (canonical param)' },
folderPath: { type: 'string', description: 'Folder path for file upload' },
fileName: { type: 'string', description: 'File name override' },
files: { type: 'array', description: 'Files to upload' },
@@ -537,6 +631,12 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
description:
'An array of SharePoint site objects, each containing details such as id, name, and more.',
},
+ page: {
+ type: 'json',
+ description: 'SharePoint page object (id, name, title, webUrl, pageLayout)',
+ },
+ published: { type: 'boolean', description: 'Whether the page was published' },
+ deleted: { type: 'boolean', description: 'Whether the item/page/file was deleted' },
list: {
type: 'json',
description: 'SharePoint list object (id, displayName, name, webUrl, etc.)',
@@ -549,6 +649,14 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
type: 'json',
description: 'Array of SharePoint list items with fields',
},
+ driveItem: {
+ type: 'json',
+ description: 'SharePoint drive item metadata (id, name, size, webUrl, file/folder facet)',
+ },
+ file: {
+ type: 'json',
+ description: 'Downloaded file stored in execution files',
+ },
uploadedFiles: {
type: 'json',
description: 'Array of uploaded file objects with id, name, webUrl, size',
@@ -557,6 +665,8 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
type: 'number',
description: 'Number of files uploaded',
},
+ itemId: { type: 'string', description: 'ID of the deleted list item or file' },
+ pageId: { type: 'string', description: 'ID of the deleted or published page' },
success: {
type: 'boolean',
description: 'Success status',
@@ -571,21 +681,48 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
const SHAREPOINT_V2_TOOL_IDS = [
'sharepoint_create_page',
'sharepoint_read_page',
+ 'sharepoint_update_page',
+ 'sharepoint_publish_page',
+ 'sharepoint_delete_page',
'sharepoint_list_sites',
'sharepoint_create_list',
'sharepoint_get_list',
'sharepoint_update_list',
'sharepoint_add_list_items',
+ 'sharepoint_get_list_item',
+ 'sharepoint_delete_list_item',
'sharepoint_upload_file',
+ 'sharepoint_download_file',
+ 'sharepoint_get_drive_item',
+ 'sharepoint_delete_file',
+] as const
+
+const SHAREPOINT_V2_DRIVE_ONLY_OPERATIONS = [
+ 'sharepoint_download_file',
+ 'sharepoint_delete_file',
+ 'sharepoint_get_drive_item',
] as const
-const SHAREPOINT_V2_SITE_OPERATIONS = Array.from(SHAREPOINT_V2_TOOL_IDS)
+const SHAREPOINT_V2_SITE_OPERATIONS = SHAREPOINT_V2_TOOL_IDS.filter(
+ (id) => !(SHAREPOINT_V2_DRIVE_ONLY_OPERATIONS as readonly string[]).includes(id)
+)
const SHAREPOINT_V2_LIST_ITEM_OPERATIONS = [
'sharepoint_update_list',
'sharepoint_add_list_items',
] as const
+const SHAREPOINT_V2_LIST_ITEM_LOOKUP_OPERATIONS = [
+ 'sharepoint_get_list_item',
+ 'sharepoint_delete_list_item',
+] as const
+
+const SHAREPOINT_V2_PAGE_MUTATION_OPERATIONS = [
+ 'sharepoint_update_page',
+ 'sharepoint_publish_page',
+ 'sharepoint_delete_page',
+] as const
+
export const SharepointV2Block: BlockConfig = {
...SharepointBlock,
type: 'sharepoint_v2',
@@ -599,12 +736,20 @@ export const SharepointV2Block: BlockConfig = {
options: [
{ label: 'Create Page', id: 'sharepoint_create_page' },
{ label: 'Read Page', id: 'sharepoint_read_page' },
+ { label: 'Update Page', id: 'sharepoint_update_page' },
+ { label: 'Publish Page', id: 'sharepoint_publish_page' },
+ { label: 'Delete Page', id: 'sharepoint_delete_page' },
{ label: 'List Sites', id: 'sharepoint_list_sites' },
{ label: 'Create List', id: 'sharepoint_create_list' },
{ label: 'Read List', id: 'sharepoint_get_list' },
{ label: 'Update List Item', id: 'sharepoint_update_list' },
{ label: 'Add List Item', id: 'sharepoint_add_list_items' },
+ { label: 'Get List Item', id: 'sharepoint_get_list_item' },
+ { label: 'Delete List Item', id: 'sharepoint_delete_list_item' },
{ label: 'Upload File', id: 'sharepoint_upload_file' },
+ { label: 'Download File', id: 'sharepoint_download_file' },
+ { label: 'Get Drive Item', id: 'sharepoint_get_drive_item' },
+ { label: 'Delete File', id: 'sharepoint_delete_file' },
],
value: () => 'sharepoint_create_page',
},
@@ -664,8 +809,12 @@ export const SharepointV2Block: BlockConfig = {
id: 'pageId',
title: 'Page ID',
type: 'short-input',
- placeholder: 'Page ID (alternative to page name)',
- condition: { field: 'operation', value: 'sharepoint_read_page' },
+ placeholder: 'Page ID (alternative to page name for Read Page)',
+ condition: {
+ field: 'operation',
+ value: ['sharepoint_read_page', ...SHAREPOINT_V2_PAGE_MUTATION_OPERATIONS],
+ },
+ required: { field: 'operation', value: [...SHAREPOINT_V2_PAGE_MUTATION_OPERATIONS] },
mode: 'advanced',
},
{
@@ -673,7 +822,10 @@ export const SharepointV2Block: BlockConfig = {
title: 'Page Title',
type: 'short-input',
placeholder: 'Optional title (defaults to page name)',
- condition: { field: 'operation', value: 'sharepoint_create_page' },
+ condition: {
+ field: 'operation',
+ value: ['sharepoint_create_page', 'sharepoint_update_page'],
+ },
mode: 'advanced',
},
{
@@ -681,7 +833,10 @@ export const SharepointV2Block: BlockConfig = {
title: 'Page Content',
type: 'long-input',
placeholder: 'Optional text content for the page',
- condition: { field: 'operation', value: 'sharepoint_create_page' },
+ condition: {
+ field: 'operation',
+ value: ['sharepoint_create_page', 'sharepoint_update_page'],
+ },
mode: 'advanced',
},
{
@@ -712,9 +867,19 @@ export const SharepointV2Block: BlockConfig = {
mode: 'basic',
condition: {
field: 'operation',
- value: ['sharepoint_get_list', ...SHAREPOINT_V2_LIST_ITEM_OPERATIONS],
+ value: [
+ 'sharepoint_get_list',
+ ...SHAREPOINT_V2_LIST_ITEM_OPERATIONS,
+ ...SHAREPOINT_V2_LIST_ITEM_LOOKUP_OPERATIONS,
+ ],
+ },
+ required: {
+ field: 'operation',
+ value: [
+ ...SHAREPOINT_V2_LIST_ITEM_OPERATIONS,
+ ...SHAREPOINT_V2_LIST_ITEM_LOOKUP_OPERATIONS,
+ ],
},
- required: { field: 'operation', value: [...SHAREPOINT_V2_LIST_ITEM_OPERATIONS] },
},
{
id: 'manualListId',
@@ -725,9 +890,19 @@ export const SharepointV2Block: BlockConfig = {
mode: 'advanced',
condition: {
field: 'operation',
- value: ['sharepoint_get_list', ...SHAREPOINT_V2_LIST_ITEM_OPERATIONS],
+ value: [
+ 'sharepoint_get_list',
+ ...SHAREPOINT_V2_LIST_ITEM_OPERATIONS,
+ ...SHAREPOINT_V2_LIST_ITEM_LOOKUP_OPERATIONS,
+ ],
+ },
+ required: {
+ field: 'operation',
+ value: [
+ ...SHAREPOINT_V2_LIST_ITEM_OPERATIONS,
+ ...SHAREPOINT_V2_LIST_ITEM_LOOKUP_OPERATIONS,
+ ],
},
- required: { field: 'operation', value: [...SHAREPOINT_V2_LIST_ITEM_OPERATIONS] },
},
{
id: 'includeColumns',
@@ -821,8 +996,14 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
title: 'Item ID',
type: 'short-input',
placeholder: 'Enter item ID',
- condition: { field: 'operation', value: 'sharepoint_update_list' },
- required: { field: 'operation', value: 'sharepoint_update_list' },
+ condition: {
+ field: 'operation',
+ value: ['sharepoint_update_list', ...SHAREPOINT_V2_LIST_ITEM_LOOKUP_OPERATIONS],
+ },
+ required: {
+ field: 'operation',
+ value: ['sharepoint_update_list', ...SHAREPOINT_V2_LIST_ITEM_LOOKUP_OPERATIONS],
+ },
},
{
id: 'listItemFields',
@@ -848,9 +1029,21 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
title: 'Document Library ID',
type: 'short-input',
placeholder: 'Enter document library (drive) ID',
- condition: { field: 'operation', value: 'sharepoint_upload_file' },
+ condition: {
+ field: 'operation',
+ value: [...SHAREPOINT_V2_DRIVE_ONLY_OPERATIONS, 'sharepoint_upload_file'],
+ },
+ required: { field: 'operation', value: [...SHAREPOINT_V2_DRIVE_ONLY_OPERATIONS] },
mode: 'advanced',
},
+ {
+ id: 'driveItemId',
+ title: 'File ID',
+ type: 'short-input',
+ placeholder: 'Enter the file (drive item) ID',
+ condition: { field: 'operation', value: [...SHAREPOINT_V2_DRIVE_ONLY_OPERATIONS] },
+ required: { field: 'operation', value: [...SHAREPOINT_V2_DRIVE_ONLY_OPERATIONS] },
+ },
{
id: 'folderPath',
title: 'Folder Path',
@@ -864,8 +1057,11 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
id: 'fileName',
title: 'File Name',
type: 'short-input',
- placeholder: 'Optional: override uploaded file name',
- condition: { field: 'operation', value: 'sharepoint_upload_file' },
+ placeholder: 'Optional: override uploaded/downloaded file name',
+ condition: {
+ field: 'operation',
+ value: ['sharepoint_upload_file', 'sharepoint_download_file'],
+ },
mode: 'advanced',
required: false,
},
@@ -919,6 +1115,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
files,
maxPages,
driveId,
+ driveItemId,
...rest
} = params
@@ -950,6 +1147,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
listId: cleanString(listId),
itemId: cleanString(itemId),
driveId: cleanString(driveId),
+ driveItemId: cleanString(driveItemId),
includeColumns: coerceBoolean(includeColumns),
includeItems: coerceBoolean(includeItems),
listItemFields: parseJsonObject(listItemFields),
@@ -991,6 +1189,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
itemId: { type: 'string', description: 'List item ID' },
listItemFields: { type: 'json', description: 'List item fields' },
driveId: { type: 'string', description: 'Document library (drive) ID' },
+ driveItemId: { type: 'string', description: 'File (drive item) ID' },
folderPath: { type: 'string', description: 'Folder path for file upload' },
fileName: { type: 'string', description: 'File name override' },
files: { type: 'json', description: 'Files to upload' },
@@ -1017,6 +1216,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
description: 'SharePoint page content (content, canvasLayout)',
},
totalPages: { type: 'number', description: 'Number of pages returned' },
+ published: { type: 'boolean', description: 'Whether the page was published' },
list: {
type: 'json',
description: 'SharePoint list object (id, displayName, name, webUrl, columns, items)',
@@ -1027,6 +1227,15 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
},
item: { type: 'json', description: 'SharePoint list item with fields' },
items: { type: 'json', description: 'Array of SharePoint list items with fields' },
+ deleted: { type: 'boolean', description: 'Whether the item/page/file was deleted' },
+ driveItem: {
+ type: 'json',
+ description: 'SharePoint drive item metadata (id, name, size, webUrl, file/folder facet)',
+ },
+ file: {
+ type: 'json',
+ description: 'Downloaded file stored in execution files',
+ },
uploadedFiles: {
type: 'json',
description: 'Array of uploaded file objects with id, name, webUrl, size',
@@ -1041,6 +1250,8 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
type: 'json',
description: 'Array of per-file upload errors (name, error, status)',
},
+ itemId: { type: 'string', description: 'ID of the deleted list item or file' },
+ pageId: { type: 'string', description: 'ID of the deleted or published page' },
nextPageUrl: {
type: 'string',
description: 'Microsoft Graph @odata.nextLink URL for the next page of results',
diff --git a/apps/sim/lib/api/contracts/tools/microsoft.ts b/apps/sim/lib/api/contracts/tools/microsoft.ts
index be68501cdba..30fe918d9cf 100644
--- a/apps/sim/lib/api/contracts/tools/microsoft.ts
+++ b/apps/sim/lib/api/contracts/tools/microsoft.ts
@@ -86,6 +86,13 @@ export const sharepointUploadBodySchema = z.object({
files: RawFileInputArraySchema.optional().nullable(),
})
+export const sharepointDownloadFileBodySchema = z.object({
+ accessToken: accessTokenSchema,
+ driveId: z.string().min(1, 'Drive ID is required'),
+ itemId: z.string().min(1, 'Item ID is required'),
+ fileName: z.string().optional().nullable(),
+})
+
export const dataverseUploadFileBodySchema = z.object({
accessToken: accessTokenSchema,
environmentUrl: z.string().min(1, 'Environment URL is required'),
@@ -190,6 +197,13 @@ export const sharepointUploadContract = defineRouteContract({
response: { mode: 'json', schema: toolJsonResponseSchema },
})
+export const sharepointDownloadFileContract = defineRouteContract({
+ method: 'POST',
+ path: '/api/tools/sharepoint/download-file',
+ body: sharepointDownloadFileBodySchema,
+ response: { mode: 'json', schema: toolJsonResponseSchema },
+})
+
export const dataverseUploadFileContract = defineRouteContract({
method: 'POST',
path: '/api/tools/microsoft-dataverse/upload-file',
@@ -210,4 +224,5 @@ export type TeamsDeleteChatMessageBody = ContractBody
export type OneDriveDownloadBody = ContractBody
export type SharepointUploadBody = ContractBody
+export type SharepointDownloadFileBody = ContractBody
export type DataverseUploadFileBody = z.output
diff --git a/apps/sim/lib/core/security/input-validation.server.ts b/apps/sim/lib/core/security/input-validation.server.ts
index a81853c5514..7f81039622c 100644
--- a/apps/sim/lib/core/security/input-validation.server.ts
+++ b/apps/sim/lib/core/security/input-validation.server.ts
@@ -4,6 +4,7 @@ import https from 'https'
import type { LookupFunction } from 'net'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
+import { omit } from '@sim/utils/object'
import * as ipaddr from 'ipaddr.js'
import { Agent, type RequestInit as UndiciRequestInit, fetch as undiciFetch } from 'undici'
import { isHosted, isPrivateDatabaseHostsAllowed } from '@/lib/core/config/env-flags'
@@ -333,6 +334,8 @@ export interface SecureFetchOptions {
maxRedirects?: number
maxResponseBytes?: number
signal?: AbortSignal
+ /** Drop the Authorization header when following a redirect, so it is not sent to the redirect target's origin. */
+ stripAuthOnRedirect?: boolean
}
export class SecureFetchHeaders {
@@ -500,10 +503,16 @@ export async function secureFetchWithPinnedIP(
settledReject(new Error(`Redirect blocked: ${validation.error}`))
return
}
+ const redirectOptions = options.stripAuthOnRedirect
+ ? {
+ ...options,
+ headers: omit(options.headers ?? {}, ['Authorization', 'authorization']),
+ }
+ : options
return secureFetchWithPinnedIP(
redirectUrl,
validation.resolvedIP!,
- options,
+ redirectOptions,
redirectCount + 1
)
})
diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts
index 2bb910c4eef..2935858eddb 100644
--- a/apps/sim/tools/registry.ts
+++ b/apps/sim/tools/registry.ts
@@ -3160,10 +3160,18 @@ import {
sharepointAddListItemTool,
sharepointCreateListTool,
sharepointCreatePageTool,
+ sharepointDeleteFileTool,
+ sharepointDeleteListItemTool,
+ sharepointDeletePageTool,
+ sharepointDownloadFileTool,
+ sharepointGetDriveItemTool,
+ sharepointGetListItemTool,
sharepointGetListTool,
sharepointListSitesTool,
+ sharepointPublishPageTool,
sharepointReadPageTool,
sharepointUpdateListItemTool,
+ sharepointUpdatePageTool,
sharepointUploadFileTool,
} from '@/tools/sharepoint'
import {
@@ -7636,10 +7644,18 @@ export const tools: Record = {
sharepoint_add_list_items: sharepointAddListItemTool,
sharepoint_create_list: sharepointCreateListTool,
sharepoint_create_page: sharepointCreatePageTool,
+ sharepoint_delete_file: sharepointDeleteFileTool,
+ sharepoint_delete_list_item: sharepointDeleteListItemTool,
+ sharepoint_delete_page: sharepointDeletePageTool,
+ sharepoint_download_file: sharepointDownloadFileTool,
+ sharepoint_get_drive_item: sharepointGetDriveItemTool,
sharepoint_get_list: sharepointGetListTool,
+ sharepoint_get_list_item: sharepointGetListItemTool,
sharepoint_list_sites: sharepointListSitesTool,
+ sharepoint_publish_page: sharepointPublishPageTool,
sharepoint_read_page: sharepointReadPageTool,
sharepoint_update_list: sharepointUpdateListItemTool,
+ sharepoint_update_page: sharepointUpdatePageTool,
sharepoint_upload_file: sharepointUploadFileTool,
stripe_create_payment_intent: stripeCreatePaymentIntentTool,
stripe_retrieve_payment_intent: stripeRetrievePaymentIntentTool,
diff --git a/apps/sim/tools/sharepoint/add_list_items.ts b/apps/sim/tools/sharepoint/add_list_items.ts
index ab3afdf3bea..1958a0fe9e6 100644
--- a/apps/sim/tools/sharepoint/add_list_items.ts
+++ b/apps/sim/tools/sharepoint/add_list_items.ts
@@ -1,9 +1,28 @@
-import { createLogger } from '@sim/logger'
import type { SharepointAddListItemResponse, SharepointToolParams } from '@/tools/sharepoint/types'
-import { optionalTrim } from '@/tools/sharepoint/utils'
+import { optionalTrim, sanitizeListItemFields } from '@/tools/sharepoint/utils'
import type { ToolConfig } from '@/tools/types'
-const logger = createLogger('SharePointAddListItem')
+function resolveSanitizedFields(
+ listItemFields: SharepointToolParams['listItemFields']
+): Record {
+ if (!listItemFields || Object.keys(listItemFields).length === 0) {
+ throw new Error('listItemFields must not be empty')
+ }
+
+ const providedFields =
+ typeof listItemFields === 'object' &&
+ listItemFields !== null &&
+ 'fields' in (listItemFields as Record) &&
+ Object.keys(listItemFields as Record).length === 1
+ ? ((listItemFields as { fields: Record }).fields as Record)
+ : (listItemFields as Record)
+
+ if (!providedFields || Object.keys(providedFields).length === 0) {
+ throw new Error('No fields provided to create the SharePoint list item')
+ }
+
+ return sanitizeListItemFields(providedFields, { action: 'create' })
+}
export const addListItemTool: ToolConfig = {
id: 'sharepoint_add_list_items',
@@ -66,87 +85,23 @@ export const addListItemTool: ToolConfig {
- if (!params.listItemFields || Object.keys(params.listItemFields).length === 0) {
- throw new Error('listItemFields must not be empty')
- }
-
- const providedFields =
- typeof params.listItemFields === 'object' &&
- params.listItemFields !== null &&
- 'fields' in (params.listItemFields as Record) &&
- Object.keys(params.listItemFields as Record).length === 1
- ? ((params.listItemFields as { fields: Record }).fields as Record<
- string,
- unknown
- >)
- : (params.listItemFields as Record)
-
- if (!providedFields || Object.keys(providedFields).length === 0) {
- throw new Error('No fields provided to create the SharePoint list item')
- }
-
- const readOnlyFields = new Set([
- 'Id',
- 'id',
- 'UniqueId',
- 'GUID',
- 'ContentTypeId',
- 'Created',
- 'Modified',
- 'Author',
- 'Editor',
- 'CreatedBy',
- 'ModifiedBy',
- 'AuthorId',
- 'EditorId',
- '_UIVersionString',
- 'Attachments',
- 'FileRef',
- 'FileDirRef',
- 'FileLeafRef',
- ])
-
- const entries = Object.entries(providedFields)
- const creatableEntries = entries.filter(([key]) => !readOnlyFields.has(key))
-
- if (creatableEntries.length !== entries.length) {
- const removed = entries.filter(([key]) => readOnlyFields.has(key)).map(([key]) => key)
- logger.warn('Removed read-only SharePoint fields from create', {
- removed,
- })
- }
-
- if (creatableEntries.length === 0) {
- const requestedKeys = Object.keys(providedFields)
- throw new Error(
- `All provided fields are read-only and cannot be set: ${requestedKeys.join(', ')}`
- )
- }
-
- const sanitizedFields = Object.fromEntries(creatableEntries)
-
- logger.info('Creating SharePoint list item', {
- listId: params.listId,
- fieldsKeys: Object.keys(sanitizedFields),
- })
-
- return {
- fields: sanitizedFields,
- }
- },
+ body: (params) => ({
+ fields: resolveSanitizedFields(params.listItemFields),
+ }),
},
transformResponse: async (response: Response, params) => {
- let data: any
+ let data: Record | undefined
try {
data = await response.json()
} catch {
data = undefined
}
- const itemId: string | undefined = data?.id
- const fields: Record | undefined = data?.fields || params?.listItemFields
+ const itemId = data?.id as string | undefined
+ const fields =
+ (data?.fields as Record | undefined) ||
+ (params ? resolveSanitizedFields(params.listItemFields) : undefined)
return {
success: true,
diff --git a/apps/sim/tools/sharepoint/create_list.ts b/apps/sim/tools/sharepoint/create_list.ts
index 007631b207e..1fb20f65180 100644
--- a/apps/sim/tools/sharepoint/create_list.ts
+++ b/apps/sim/tools/sharepoint/create_list.ts
@@ -70,7 +70,7 @@ export const createListTool: ToolConfig {
- const siteId = optionalTrim(params.siteSelector) || optionalTrim(params.siteId) || 'root'
+ const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
return `https://graph.microsoft.com/v1.0/sites/${siteId}/lists`
},
method: 'POST',
diff --git a/apps/sim/tools/sharepoint/delete_file.ts b/apps/sim/tools/sharepoint/delete_file.ts
new file mode 100644
index 00000000000..183ac484762
--- /dev/null
+++ b/apps/sim/tools/sharepoint/delete_file.ts
@@ -0,0 +1,66 @@
+import type { SharepointDeleteFileResponse, SharepointToolParams } from '@/tools/sharepoint/types'
+import { optionalTrim } from '@/tools/sharepoint/utils'
+import type { ToolConfig } from '@/tools/types'
+
+export const deleteFileTool: ToolConfig = {
+ id: 'sharepoint_delete_file',
+ name: 'Delete SharePoint File',
+ description: 'Delete a file (or folder) from a SharePoint document library',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'sharepoint',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'The access token for the SharePoint API',
+ },
+ driveId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the document library (drive). Example: b!abc123def456',
+ },
+ driveItemId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the file (drive item) to delete',
+ },
+ },
+
+ request: {
+ url: (params) => {
+ const driveId = optionalTrim(params.driveId)
+ const driveItemId = optionalTrim(params.driveItemId)
+ if (!driveId) throw new Error('driveId must be provided')
+ if (!driveItemId) throw new Error('driveItemId must be provided')
+ return `https://graph.microsoft.com/v1.0/drives/${encodeURIComponent(driveId)}/items/${encodeURIComponent(driveItemId)}`
+ },
+ method: 'DELETE',
+ headers: (params) => ({
+ Authorization: `Bearer ${params.accessToken}`,
+ Accept: 'application/json',
+ }),
+ },
+
+ transformResponse: async (_response: Response, params) => {
+ return {
+ success: true,
+ output: {
+ deleted: true,
+ itemId: params?.driveItemId ?? '',
+ },
+ }
+ },
+
+ outputs: {
+ deleted: { type: 'boolean', description: 'Whether the file was deleted' },
+ itemId: { type: 'string', description: 'The ID of the deleted file' },
+ },
+}
diff --git a/apps/sim/tools/sharepoint/delete_list_item.ts b/apps/sim/tools/sharepoint/delete_list_item.ts
new file mode 100644
index 00000000000..b71e6b4e823
--- /dev/null
+++ b/apps/sim/tools/sharepoint/delete_list_item.ts
@@ -0,0 +1,88 @@
+import type {
+ SharepointDeleteListItemResponse,
+ SharepointToolParams,
+} from '@/tools/sharepoint/types'
+import { optionalTrim } from '@/tools/sharepoint/utils'
+import type { ToolConfig } from '@/tools/types'
+
+export const deleteListItemTool: ToolConfig<
+ SharepointToolParams,
+ SharepointDeleteListItemResponse
+> = {
+ id: 'sharepoint_delete_list_item',
+ name: 'Delete SharePoint List Item',
+ description: 'Delete an item from a SharePoint list',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'sharepoint',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'The access token for the SharePoint API',
+ },
+ siteSelector: {
+ type: 'string',
+ required: false,
+ visibility: 'user-only',
+ description: 'Select the SharePoint site',
+ },
+ siteId: {
+ type: 'string',
+ required: false,
+ visibility: 'hidden',
+ description: 'The ID of the SharePoint site (internal use)',
+ },
+ listId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description:
+ 'The ID of the list containing the item. Example: b!abc123def456 or a GUID like 12345678-1234-1234-1234-123456789012',
+ },
+ itemId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the list item to delete. Example: 1, 42, or 123',
+ },
+ },
+
+ request: {
+ url: (params) => {
+ const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
+ const listId = optionalTrim(params.listId)
+ const itemId = optionalTrim(params.itemId)
+ if (!listId) throw new Error('listId must be provided')
+ if (!itemId) throw new Error('itemId must be provided')
+ const listSegment = encodeURIComponent(listId)
+ const itemSegment = encodeURIComponent(itemId)
+ return `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${listSegment}/items/${itemSegment}`
+ },
+ method: 'DELETE',
+ headers: (params) => ({
+ Authorization: `Bearer ${params.accessToken}`,
+ Accept: 'application/json',
+ }),
+ },
+
+ transformResponse: async (_response: Response, params) => {
+ return {
+ success: true,
+ output: {
+ deleted: true,
+ itemId: params?.itemId ?? '',
+ },
+ }
+ },
+
+ outputs: {
+ deleted: { type: 'boolean', description: 'Whether the list item was deleted' },
+ itemId: { type: 'string', description: 'The ID of the deleted list item' },
+ },
+}
diff --git a/apps/sim/tools/sharepoint/delete_page.ts b/apps/sim/tools/sharepoint/delete_page.ts
new file mode 100644
index 00000000000..4b5b77872eb
--- /dev/null
+++ b/apps/sim/tools/sharepoint/delete_page.ts
@@ -0,0 +1,72 @@
+import type { SharepointDeletePageResponse, SharepointToolParams } from '@/tools/sharepoint/types'
+import { optionalTrim } from '@/tools/sharepoint/utils'
+import type { ToolConfig } from '@/tools/types'
+
+export const deletePageTool: ToolConfig = {
+ id: 'sharepoint_delete_page',
+ name: 'Delete SharePoint Page',
+ description: 'Delete a page from a SharePoint site',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'sharepoint',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'The access token for the SharePoint API',
+ },
+ siteSelector: {
+ type: 'string',
+ required: false,
+ visibility: 'user-only',
+ description: 'Select the SharePoint site',
+ },
+ siteId: {
+ type: 'string',
+ required: false,
+ visibility: 'hidden',
+ description: 'The ID of the SharePoint site (internal use)',
+ },
+ pageId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description:
+ 'The ID of the page to delete. Example: a GUID like 12345678-1234-1234-1234-123456789012',
+ },
+ },
+
+ request: {
+ url: (params) => {
+ const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
+ const pageId = optionalTrim(params.pageId)
+ if (!pageId) throw new Error('pageId must be provided')
+ return `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/${encodeURIComponent(pageId)}`
+ },
+ method: 'DELETE',
+ headers: (params) => ({
+ Authorization: `Bearer ${params.accessToken}`,
+ Accept: 'application/json',
+ }),
+ },
+
+ transformResponse: async (_response: Response, params) => {
+ return {
+ success: true,
+ output: {
+ deleted: true,
+ pageId: params?.pageId ?? '',
+ },
+ }
+ },
+
+ outputs: {
+ deleted: { type: 'boolean', description: 'Whether the page was deleted' },
+ pageId: { type: 'string', description: 'The ID of the deleted page' },
+ },
+}
diff --git a/apps/sim/tools/sharepoint/download_file.ts b/apps/sim/tools/sharepoint/download_file.ts
new file mode 100644
index 00000000000..add8673b254
--- /dev/null
+++ b/apps/sim/tools/sharepoint/download_file.ts
@@ -0,0 +1,59 @@
+import type { SharepointDownloadFileResponse, SharepointToolParams } from '@/tools/sharepoint/types'
+import type { ToolConfig } from '@/tools/types'
+
+export const downloadFileTool: ToolConfig = {
+ id: 'sharepoint_download_file',
+ name: 'Download File from SharePoint',
+ description: 'Download a file from a SharePoint document library',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'sharepoint',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'The access token for the SharePoint API',
+ },
+ driveId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the document library (drive). Example: b!abc123def456',
+ },
+ driveItemId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the file (drive item) to download',
+ },
+ fileName: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Optional filename override (e.g., "report.pdf", "data.xlsx")',
+ },
+ },
+
+ request: {
+ url: '/api/tools/sharepoint/download-file',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params) => ({
+ accessToken: params.accessToken,
+ driveId: params.driveId,
+ itemId: params.driveItemId,
+ fileName: params.fileName,
+ }),
+ },
+
+ outputs: {
+ file: { type: 'file', description: 'Downloaded file stored in execution files' },
+ },
+}
diff --git a/apps/sim/tools/sharepoint/get_drive_item.ts b/apps/sim/tools/sharepoint/get_drive_item.ts
new file mode 100644
index 00000000000..0b6f689aaf6
--- /dev/null
+++ b/apps/sim/tools/sharepoint/get_drive_item.ts
@@ -0,0 +1,106 @@
+import type {
+ SharepointDriveItem,
+ SharepointGetDriveItemResponse,
+ SharepointToolParams,
+} from '@/tools/sharepoint/types'
+import { optionalTrim } from '@/tools/sharepoint/utils'
+import type { ToolConfig } from '@/tools/types'
+
+export const getDriveItemTool: ToolConfig = {
+ id: 'sharepoint_get_drive_item',
+ name: 'Get SharePoint Drive Item',
+ description: 'Get metadata for a file or folder in a SharePoint document library',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'sharepoint',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'The access token for the SharePoint API',
+ },
+ driveId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the document library (drive). Example: b!abc123def456',
+ },
+ driveItemId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the file or folder (drive item) to retrieve',
+ },
+ },
+
+ request: {
+ url: (params) => {
+ const driveId = optionalTrim(params.driveId)
+ const driveItemId = optionalTrim(params.driveItemId)
+ if (!driveId) throw new Error('driveId must be provided')
+ if (!driveItemId) throw new Error('driveItemId must be provided')
+ return `https://graph.microsoft.com/v1.0/drives/${encodeURIComponent(driveId)}/items/${encodeURIComponent(driveItemId)}`
+ },
+ method: 'GET',
+ headers: (params) => ({
+ Authorization: `Bearer ${params.accessToken}`,
+ Accept: 'application/json',
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const data: Record = await response.json()
+
+ const driveItem: SharepointDriveItem = {
+ id: data.id as string,
+ name: data.name as string,
+ webUrl: data.webUrl as string | undefined,
+ size: data.size as number | undefined,
+ createdDateTime: data.createdDateTime as string | undefined,
+ lastModifiedDateTime: data.lastModifiedDateTime as string | undefined,
+ file: (data.file as SharepointDriveItem['file']) ?? null,
+ folder: (data.folder as SharepointDriveItem['folder']) ?? null,
+ parentReference: (data.parentReference as SharepointDriveItem['parentReference']) ?? null,
+ }
+
+ return {
+ success: true,
+ output: { driveItem },
+ }
+ },
+
+ outputs: {
+ driveItem: {
+ type: 'object',
+ description: 'Metadata for the SharePoint file or folder',
+ properties: {
+ id: { type: 'string', description: 'The unique ID of the drive item' },
+ name: { type: 'string', description: 'The name of the file or folder' },
+ webUrl: { type: 'string', description: 'The URL to access the item' },
+ size: { type: 'number', description: 'The size of the item in bytes', optional: true },
+ createdDateTime: { type: 'string', description: 'When the item was created' },
+ lastModifiedDateTime: { type: 'string', description: 'When the item was last modified' },
+ file: {
+ type: 'object',
+ description: 'Present if the item is a file (contains mimeType)',
+ optional: true,
+ },
+ folder: {
+ type: 'object',
+ description: 'Present if the item is a folder (contains childCount)',
+ optional: true,
+ },
+ parentReference: {
+ type: 'object',
+ description: 'Reference to the parent folder/drive',
+ optional: true,
+ },
+ },
+ },
+ },
+}
diff --git a/apps/sim/tools/sharepoint/get_list.ts b/apps/sim/tools/sharepoint/get_list.ts
index bcf3036a977..5b1d4d44a21 100644
--- a/apps/sim/tools/sharepoint/get_list.ts
+++ b/apps/sim/tools/sharepoint/get_list.ts
@@ -107,7 +107,7 @@ export const getListTool: ToolConfig 0) url.searchParams.append('$expand', expandParts.join(','))
const finalUrl = url.toString()
@@ -128,21 +128,23 @@ export const getListTool: ToolConfig {
- const data = await response.json()
+ const data: Record = await response.json()
+ const value = data.value
// If the response is a collection of items (from the items endpoint)
if (
- Array.isArray((data as any).value) &&
- (data as any).value.length > 0 &&
- (data as any).value[0] &&
- 'fields' in (data as any).value[0]
+ Array.isArray(value) &&
+ value.length > 0 &&
+ value[0] &&
+ typeof value[0] === 'object' &&
+ 'fields' in value[0]
) {
- const items = (data as any).value.map((i: any) => ({
- id: i.id,
+ const items = value.map((i: Record) => ({
+ id: i.id as string,
fields: i.fields as Record,
}))
- const nextPageUrl = getGraphNextPageUrl(data as Record)
+ const nextPageUrl = getGraphNextPageUrl(data)
return {
success: true,
@@ -154,18 +156,18 @@ export const getListTool: ToolConfig ({
- id: l.id,
- displayName: l.displayName ?? l.name,
- name: l.name,
- webUrl: l.webUrl,
- createdDateTime: l.createdDateTime,
- lastModifiedDateTime: l.lastModifiedDateTime,
- list: l.list,
+ if (Array.isArray(value)) {
+ const lists: SharepointList[] = value.map((l: Record) => ({
+ id: l.id as string,
+ displayName: (l.displayName ?? l.name) as string | undefined,
+ name: l.name as string | undefined,
+ webUrl: l.webUrl as string | undefined,
+ createdDateTime: l.createdDateTime as string | undefined,
+ lastModifiedDateTime: l.lastModifiedDateTime as string | undefined,
+ list: l.list as SharepointList['list'],
}))
- const nextPageUrl = getGraphNextPageUrl(data as Record)
+ const nextPageUrl = getGraphNextPageUrl(data)
return {
success: true,
@@ -174,29 +176,32 @@ export const getListTool: ToolConfig ({
- id: c.id,
- name: c.name,
- displayName: c.displayName,
- description: c.description,
- indexed: c.indexed,
- enforcedUniqueValues: c.enforcedUniqueValues,
- hidden: c.hidden,
- readOnly: c.readOnly,
- required: c.required,
- columnGroup: c.columnGroup,
+ ? data.columns.map((c: Record) => ({
+ id: c.id as string | undefined,
+ name: c.name as string | undefined,
+ displayName: c.displayName as string | undefined,
+ description: c.description as string | undefined,
+ indexed: c.indexed as boolean | undefined,
+ enforcedUniqueValues: c.enforcedUniqueValues as boolean | undefined,
+ hidden: c.hidden as boolean | undefined,
+ readOnly: c.readOnly as boolean | undefined,
+ required: c.required as boolean | undefined,
+ columnGroup: c.columnGroup as string | undefined,
}))
: undefined,
items: Array.isArray(data.items)
- ? data.items.map((i: any) => ({ id: i.id, fields: i.fields as Record }))
+ ? data.items.map((i: Record) => ({
+ id: i.id as string,
+ fields: i.fields as Record,
+ }))
: undefined,
}
diff --git a/apps/sim/tools/sharepoint/get_list_item.ts b/apps/sim/tools/sharepoint/get_list_item.ts
new file mode 100644
index 00000000000..1423d8b1b14
--- /dev/null
+++ b/apps/sim/tools/sharepoint/get_list_item.ts
@@ -0,0 +1,96 @@
+import type { SharepointGetListItemResponse, SharepointToolParams } from '@/tools/sharepoint/types'
+import { optionalTrim } from '@/tools/sharepoint/utils'
+import type { ToolConfig } from '@/tools/types'
+
+export const getListItemTool: ToolConfig = {
+ id: 'sharepoint_get_list_item',
+ name: 'Get SharePoint List Item',
+ description: 'Get a single item (with field values) from a SharePoint list',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'sharepoint',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'The access token for the SharePoint API',
+ },
+ siteSelector: {
+ type: 'string',
+ required: false,
+ visibility: 'user-only',
+ description: 'Select the SharePoint site',
+ },
+ siteId: {
+ type: 'string',
+ required: false,
+ visibility: 'hidden',
+ description: 'The ID of the SharePoint site (internal use)',
+ },
+ listId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description:
+ 'The ID of the list containing the item. Example: b!abc123def456 or a GUID like 12345678-1234-1234-1234-123456789012',
+ },
+ itemId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the list item to retrieve. Example: 1, 42, or 123',
+ },
+ },
+
+ request: {
+ url: (params) => {
+ const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
+ const listId = optionalTrim(params.listId)
+ const itemId = optionalTrim(params.itemId)
+ if (!listId) throw new Error('listId must be provided')
+ if (!itemId) throw new Error('itemId must be provided')
+ const listSegment = encodeURIComponent(listId)
+ const itemSegment = encodeURIComponent(itemId)
+ const url = new URL(
+ `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${listSegment}/items/${itemSegment}`
+ )
+ url.searchParams.set('$expand', 'fields')
+ return url.toString()
+ },
+ method: 'GET',
+ headers: (params) => ({
+ Authorization: `Bearer ${params.accessToken}`,
+ Accept: 'application/json',
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const data: Record = await response.json()
+
+ return {
+ success: true,
+ output: {
+ item: {
+ id: data.id as string,
+ fields: data.fields as Record | undefined,
+ },
+ },
+ }
+ },
+
+ outputs: {
+ item: {
+ type: 'object',
+ description: 'SharePoint list item with field values',
+ properties: {
+ id: { type: 'string', description: 'Item ID' },
+ fields: { type: 'object', description: 'Field values for the item' },
+ },
+ },
+ },
+}
diff --git a/apps/sim/tools/sharepoint/index.ts b/apps/sim/tools/sharepoint/index.ts
index eaa798cd192..01feb9f8618 100644
--- a/apps/sim/tools/sharepoint/index.ts
+++ b/apps/sim/tools/sharepoint/index.ts
@@ -1,19 +1,35 @@
import { addListItemTool } from '@/tools/sharepoint/add_list_items'
import { createListTool } from '@/tools/sharepoint/create_list'
import { createPageTool } from '@/tools/sharepoint/create_page'
+import { deleteFileTool } from '@/tools/sharepoint/delete_file'
+import { deleteListItemTool } from '@/tools/sharepoint/delete_list_item'
+import { deletePageTool } from '@/tools/sharepoint/delete_page'
+import { downloadFileTool } from '@/tools/sharepoint/download_file'
+import { getDriveItemTool } from '@/tools/sharepoint/get_drive_item'
import { getListTool } from '@/tools/sharepoint/get_list'
+import { getListItemTool } from '@/tools/sharepoint/get_list_item'
import { listSitesTool } from '@/tools/sharepoint/list_sites'
+import { publishPageTool } from '@/tools/sharepoint/publish_page'
import { readPageTool } from '@/tools/sharepoint/read_page'
import { updateListItemTool } from '@/tools/sharepoint/update_list'
+import { updatePageTool } from '@/tools/sharepoint/update_page'
import { uploadFileTool } from '@/tools/sharepoint/upload_file'
+export const sharepointAddListItemTool = addListItemTool
export const sharepointCreatePageTool = createPageTool
export const sharepointCreateListTool = createListTool
+export const sharepointDeleteFileTool = deleteFileTool
+export const sharepointDeleteListItemTool = deleteListItemTool
+export const sharepointDeletePageTool = deletePageTool
+export const sharepointDownloadFileTool = downloadFileTool
+export const sharepointGetDriveItemTool = getDriveItemTool
export const sharepointGetListTool = getListTool
+export const sharepointGetListItemTool = getListItemTool
export const sharepointListSitesTool = listSitesTool
+export const sharepointPublishPageTool = publishPageTool
export const sharepointReadPageTool = readPageTool
export const sharepointUpdateListItemTool = updateListItemTool
-export const sharepointAddListItemTool = addListItemTool
+export const sharepointUpdatePageTool = updatePageTool
export const sharepointUploadFileTool = uploadFileTool
export * from '@/tools/sharepoint/types'
diff --git a/apps/sim/tools/sharepoint/list_sites.ts b/apps/sim/tools/sharepoint/list_sites.ts
index 089010ac4c3..d35ecc3a03d 100644
--- a/apps/sim/tools/sharepoint/list_sites.ts
+++ b/apps/sim/tools/sharepoint/list_sites.ts
@@ -140,9 +140,9 @@ export const listSitesTool: ToolConfig = {
+ id: 'sharepoint_publish_page',
+ name: 'Publish SharePoint Page',
+ description: 'Publish the latest version of a SharePoint page, making it available to all users',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'sharepoint',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'The access token for the SharePoint API',
+ },
+ siteSelector: {
+ type: 'string',
+ required: false,
+ visibility: 'user-only',
+ description: 'Select the SharePoint site',
+ },
+ siteId: {
+ type: 'string',
+ required: false,
+ visibility: 'hidden',
+ description: 'The ID of the SharePoint site (internal use)',
+ },
+ pageId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description:
+ 'The ID of the page to publish. Example: a GUID like 12345678-1234-1234-1234-123456789012',
+ },
+ },
+
+ request: {
+ url: (params) => {
+ const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
+ const pageId = optionalTrim(params.pageId)
+ if (!pageId) throw new Error('pageId must be provided')
+ return `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/${encodeURIComponent(pageId)}/microsoft.graph.sitePage/publish`
+ },
+ method: 'POST',
+ headers: (params) => ({
+ Authorization: `Bearer ${params.accessToken}`,
+ Accept: 'application/json',
+ }),
+ },
+
+ transformResponse: async (_response: Response, params) => {
+ return {
+ success: true,
+ output: {
+ published: true,
+ pageId: params?.pageId ?? '',
+ },
+ }
+ },
+
+ outputs: {
+ published: { type: 'boolean', description: 'Whether the page was published' },
+ pageId: { type: 'string', description: 'The ID of the published page' },
+ },
+}
diff --git a/apps/sim/tools/sharepoint/read_page.ts b/apps/sim/tools/sharepoint/read_page.ts
index 554f747af0b..926ba9a1c48 100644
--- a/apps/sim/tools/sharepoint/read_page.ts
+++ b/apps/sim/tools/sharepoint/read_page.ts
@@ -85,12 +85,13 @@ export const readPageTool: ToolConfig ({ id: p.id, name: p.name, title: p.title })),
+ foundPages: data.value.map((p) => ({ id: p.id, name: p.name, title: p.title })),
totalCount: data.value.length,
})
if (params?.pageName) {
const pageData = data.value[0]
const siteId = params?.siteId || params?.siteSelector || 'root'
- const contentUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/${pageData.id}/microsoft.graph.sitePage?$expand=canvasLayout`
+ const contentUrl = `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/pages/${encodeURIComponent(pageData.id)}/microsoft.graph.sitePage?$expand=canvasLayout`
logger.info('Making API call to get page content for searched page', {
pageId: pageData.id,
@@ -262,8 +263,9 @@ export const readPageTool: ToolConfig
- }>
- webparts?: Array<{
- id: string
- innerHtml: string
- }>
- }>
- }
+ canvasLayout?: CanvasLayout
}
export interface SharepointPageContent {
content: string
- canvasLayout?: {
- horizontalSections: Array<{
- layout: string
- id: string
- emphasis: string
- webparts: Array<{
- id: string
- innerHtml: string
- }>
- }>
- } | null
+ canvasLayout?: CanvasLayout | null
}
interface SharepointColumn {
@@ -132,9 +104,8 @@ export interface SharepointReadSiteResponse extends ToolResponse {
createdDateTime?: string
lastModifiedDateTime?: string
isPersonalSite?: boolean
- root?: {
- serverRelativeUrl: string
- }
+ // Graph returns an empty object marker (not a URL) when this site is the root of its site collection
+ root?: Record
siteCollection?: {
hostname: string
}
@@ -177,14 +148,15 @@ export interface SharepointToolParams {
listDisplayName?: string
listDescription?: string
listTemplate?: string
- // Update List Item
+ // Update List Item / Delete List Item / Get List Item
itemId?: string
listItemFields?: Record
- // Upload File
+ // Upload File / Download File / Delete File / Get Drive Item
driveId?: string
folderPath?: string
fileName?: string
files?: UserFile[]
+ driveItemId?: string
}
export interface GraphApiResponse {
@@ -220,6 +192,8 @@ export interface CanvasLayout {
id?: string
emphasis?: string
columns?: Array<{
+ id?: string
+ width?: number
webparts?: Array<{
id?: string
innerHtml?: string
@@ -242,6 +216,14 @@ export type SharepointResponse =
| SharepointUpdateListItemResponse
| SharepointAddListItemResponse
| SharepointUploadFileResponse
+ | SharepointDeleteListItemResponse
+ | SharepointGetListItemResponse
+ | SharepointDeletePageResponse
+ | SharepointUpdatePageResponse
+ | SharepointPublishPageResponse
+ | SharepointDownloadFileResponse
+ | SharepointDeleteFileResponse
+ | SharepointGetDriveItemResponse
export interface SharepointGetListResponse extends ToolResponse {
output: {
@@ -307,3 +289,83 @@ export interface SharepointUploadFileResponse extends ToolResponse {
errors?: SharepointUploadError[]
}
}
+
+export interface SharepointDeleteListItemResponse extends ToolResponse {
+ output: {
+ deleted: boolean
+ itemId: string
+ }
+}
+
+export interface SharepointGetListItemResponse extends ToolResponse {
+ output: {
+ item: {
+ id: string
+ fields?: Record
+ }
+ }
+}
+
+export interface SharepointDeletePageResponse extends ToolResponse {
+ output: {
+ deleted: boolean
+ pageId: string
+ }
+}
+
+export interface SharepointUpdatePageResponse extends ToolResponse {
+ output: {
+ page: SharepointPage
+ }
+}
+
+export interface SharepointPublishPageResponse extends ToolResponse {
+ output: {
+ published: boolean
+ pageId: string
+ }
+}
+
+export interface SharepointDriveItem {
+ id: string
+ name: string
+ webUrl?: string
+ size?: number
+ createdDateTime?: string
+ lastModifiedDateTime?: string
+ file?: {
+ mimeType?: string
+ } | null
+ folder?: {
+ childCount?: number
+ } | null
+ parentReference?: {
+ id?: string
+ driveId?: string
+ path?: string
+ } | null
+}
+
+export interface SharepointDownloadFileResponse extends ToolResponse {
+ output: {
+ file: {
+ name: string
+ mimeType: string
+ data: Buffer | string
+ size: number
+ }
+ }
+}
+
+export interface SharepointDeleteFileResponse extends ToolResponse {
+ output: {
+ deleted: boolean
+ itemId: string
+ }
+}
+
+export interface SharepointGetDriveItemResponse extends ToolResponse {
+ output: {
+ driveItem: SharepointDriveItem
+ }
+}
diff --git a/apps/sim/tools/sharepoint/update_list.ts b/apps/sim/tools/sharepoint/update_list.ts
index a0511e45f2b..9fe9e1a69c3 100644
--- a/apps/sim/tools/sharepoint/update_list.ts
+++ b/apps/sim/tools/sharepoint/update_list.ts
@@ -1,13 +1,10 @@
-import { createLogger } from '@sim/logger'
import type {
SharepointToolParams,
SharepointUpdateListItemResponse,
} from '@/tools/sharepoint/types'
-import { optionalTrim } from '@/tools/sharepoint/utils'
+import { optionalTrim, sanitizeListItemFields } from '@/tools/sharepoint/utils'
import type { ToolConfig } from '@/tools/types'
-const logger = createLogger('SharePointUpdateListItem')
-
export const updateListItemTool: ToolConfig<
SharepointToolParams,
SharepointUpdateListItemResponse
@@ -85,53 +82,7 @@ export const updateListItemTool: ToolConfig<
throw new Error('listItemFields must not be empty')
}
- // Filter out system/read-only fields that cannot be updated via Graph
- const readOnlyFields = new Set([
- 'Id',
- 'id',
- 'UniqueId',
- 'GUID',
- 'ContentTypeId',
- 'Created',
- 'Modified',
- 'Author',
- 'Editor',
- 'CreatedBy',
- 'ModifiedBy',
- 'AuthorId',
- 'EditorId',
- '_UIVersionString',
- 'Attachments',
- 'FileRef',
- 'FileDirRef',
- 'FileLeafRef',
- ])
-
- const entries = Object.entries(params.listItemFields)
- const updatableEntries = entries.filter(([key]) => !readOnlyFields.has(key))
-
- if (updatableEntries.length !== entries.length) {
- const removed = entries.filter(([key]) => readOnlyFields.has(key)).map(([key]) => key)
- logger.warn('Removed read-only SharePoint fields from update', {
- removed,
- })
- }
-
- if (updatableEntries.length === 0) {
- const requestedKeys = Object.keys(params.listItemFields)
- throw new Error(
- `All provided fields are read-only and cannot be updated: ${requestedKeys.join(', ')}`
- )
- }
-
- const sanitizedFields = Object.fromEntries(updatableEntries)
-
- logger.info('Updating SharePoint list item fields', {
- listItemId: params.itemId,
- listId: params.listId,
- fieldsKeys: Object.keys(sanitizedFields),
- })
- return sanitizedFields
+ return sanitizeListItemFields(params.listItemFields, { action: 'update' })
},
},
diff --git a/apps/sim/tools/sharepoint/update_page.ts b/apps/sim/tools/sharepoint/update_page.ts
new file mode 100644
index 00000000000..7c08d9b7dae
--- /dev/null
+++ b/apps/sim/tools/sharepoint/update_page.ts
@@ -0,0 +1,168 @@
+import { createLogger } from '@sim/logger'
+import type {
+ CanvasLayout,
+ SharepointToolParams,
+ SharepointUpdatePageResponse,
+} from '@/tools/sharepoint/types'
+import { optionalTrim } from '@/tools/sharepoint/utils'
+import type { ToolConfig } from '@/tools/types'
+
+const logger = createLogger('SharePointUpdatePage')
+
+export const updatePageTool: ToolConfig = {
+ id: 'sharepoint_update_page',
+ name: 'Update SharePoint Page',
+ description: 'Update the title and/or content of a SharePoint page',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'sharepoint',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'The access token for the SharePoint API',
+ },
+ siteId: {
+ type: 'string',
+ required: false,
+ visibility: 'hidden',
+ description: 'The ID of the SharePoint site (internal use)',
+ },
+ siteSelector: {
+ type: 'string',
+ required: false,
+ visibility: 'user-only',
+ description: 'Select the SharePoint site',
+ },
+ pageId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description:
+ 'The ID of the page to update. Example: a GUID like 12345678-1234-1234-1234-123456789012',
+ },
+ pageTitle: {
+ type: 'string',
+ required: false,
+ visibility: 'user-only',
+ description: 'The new title of the page',
+ },
+ pageContent: {
+ type: 'string',
+ required: false,
+ visibility: 'user-only',
+ description:
+ 'The new text content of the page. Replaces the entire canvas layout of the page.',
+ },
+ },
+
+ request: {
+ url: (params) => {
+ const siteId = optionalTrim(params.siteSelector) || optionalTrim(params.siteId) || 'root'
+ const pageId = optionalTrim(params.pageId)
+ if (!pageId) throw new Error('pageId must be provided')
+ return `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/${encodeURIComponent(pageId)}/microsoft.graph.sitePage`
+ },
+ method: 'PATCH',
+ headers: (params) => ({
+ Authorization: `Bearer ${params.accessToken}`,
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ }),
+ body: (params) => {
+ const pageTitle = optionalTrim(params.pageTitle)
+ const pageContent = typeof params.pageContent === 'string' ? params.pageContent : undefined
+
+ if (!pageTitle && !pageContent) {
+ throw new Error('At least one of pageTitle or pageContent must be provided')
+ }
+
+ const pageData: {
+ '@odata.type': string
+ title?: string
+ canvasLayout?: CanvasLayout
+ } = {
+ '@odata.type': '#microsoft.graph.sitePage',
+ }
+ if (pageTitle) pageData.title = pageTitle
+
+ if (pageContent) {
+ pageData.canvasLayout = {
+ horizontalSections: [
+ {
+ layout: 'oneColumn',
+ id: '1',
+ emphasis: 'none',
+ columns: [
+ {
+ id: '1',
+ width: 12,
+ webparts: [
+ {
+ id: '6f9230af-2a98-4952-b205-9ede4f9ef548',
+ innerHtml: `${pageContent.replace(/"/g, '"').replace(/'/g, ''')}
`,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ }
+ }
+
+ logger.info('Updating SharePoint page', {
+ pageId: params.pageId,
+ hasTitle: !!pageTitle,
+ hasContent: !!pageContent,
+ })
+
+ return pageData
+ },
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+
+ logger.info('SharePoint page updated successfully', {
+ pageId: data.id,
+ pageName: data.name,
+ pageTitle: data.title,
+ })
+
+ return {
+ success: true,
+ output: {
+ page: {
+ id: data.id,
+ name: data.name,
+ title: data.title || data.name,
+ webUrl: data.webUrl,
+ pageLayout: data.pageLayout,
+ createdDateTime: data.createdDateTime,
+ lastModifiedDateTime: data.lastModifiedDateTime,
+ },
+ },
+ }
+ },
+
+ outputs: {
+ page: {
+ type: 'object',
+ description: 'Updated SharePoint page information',
+ properties: {
+ id: { type: 'string', description: 'The unique ID of the page' },
+ name: { type: 'string', description: 'The name of the page' },
+ title: { type: 'string', description: 'The title of the page' },
+ webUrl: { type: 'string', description: 'The URL to access the page' },
+ pageLayout: { type: 'string', description: 'The layout type of the page' },
+ createdDateTime: { type: 'string', description: 'When the page was created' },
+ lastModifiedDateTime: { type: 'string', description: 'When the page was last modified' },
+ },
+ },
+ },
+}
diff --git a/apps/sim/tools/sharepoint/utils.ts b/apps/sim/tools/sharepoint/utils.ts
index 200e67156eb..353843f370e 100644
--- a/apps/sim/tools/sharepoint/utils.ts
+++ b/apps/sim/tools/sharepoint/utils.ts
@@ -102,6 +102,57 @@ export function extractTextFromCanvasLayout(canvasLayout: CanvasLayout | null |
return finalContent
}
+/** SharePoint list item fields that are system-managed and cannot be set via the Graph API. */
+export const READ_ONLY_LIST_ITEM_FIELDS = new Set([
+ 'Id',
+ 'id',
+ 'UniqueId',
+ 'GUID',
+ 'ContentTypeId',
+ 'Created',
+ 'Modified',
+ 'Author',
+ 'Editor',
+ 'CreatedBy',
+ 'ModifiedBy',
+ 'AuthorId',
+ 'EditorId',
+ '_UIVersionString',
+ 'Attachments',
+ 'FileRef',
+ 'FileDirRef',
+ 'FileLeafRef',
+])
+
+/**
+ * Removes read-only/system-managed fields from a SharePoint list item field set, logging any
+ * fields that were stripped. Throws if no updatable fields remain.
+ */
+export function sanitizeListItemFields(
+ fields: Record,
+ context: { action: 'update' | 'create' }
+): Record {
+ const entries = Object.entries(fields)
+ const updatableEntries = entries.filter(([key]) => !READ_ONLY_LIST_ITEM_FIELDS.has(key))
+
+ if (updatableEntries.length !== entries.length) {
+ const removed = entries
+ .filter(([key]) => READ_ONLY_LIST_ITEM_FIELDS.has(key))
+ .map(([key]) => key)
+ logger.warn(`Removed read-only SharePoint fields from ${context.action}`, { removed })
+ }
+
+ if (updatableEntries.length === 0) {
+ const requestedKeys = Object.keys(fields)
+ const verb = context.action === 'update' ? 'updated' : 'set'
+ throw new Error(
+ `All provided fields are read-only and cannot be ${verb}: ${requestedKeys.join(', ')}`
+ )
+ }
+
+ return Object.fromEntries(updatableEntries)
+}
+
export function cleanODataMetadata(obj: T): T {
if (!obj || typeof obj !== 'object') return obj
diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts
index 0d0d94230aa..80f935786b0 100644
--- a/scripts/check-api-validation-contracts.ts
+++ b/scripts/check-api-validation-contracts.ts
@@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries')
const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors')
const BASELINE = {
- totalRoutes: 882,
- zodRoutes: 882,
+ totalRoutes: 883,
+ zodRoutes: 883,
nonZodRoutes: 0,
} as const
From da094764957de4d254332ebd661f66607482c991 Mon Sep 17 00:00:00 2001
From: waleed
Date: Thu, 2 Jul 2026 00:26:51 -0700
Subject: [PATCH 2/6] fix(sharepoint): encode siteId consistently and fix
create_page precedence
- URL-encode siteId/groupId everywhere it's interpolated into a Graph
request path (Graph site IDs like "host,guid,guid" contain commas that
must be encoded) - addresses Cursor Bugbot findings on update_page,
delete_page, publish_page, and applies the same fix consistently across
every other sharepoint tool for consistency
- Fix create_page's site-resolution precedence (was siteSelector before
siteId, inconsistent with every sibling tool)
---
apps/sim/tools/sharepoint/add_list_items.ts | 2 +-
apps/sim/tools/sharepoint/create_list.ts | 2 +-
apps/sim/tools/sharepoint/create_page.ts | 4 ++--
apps/sim/tools/sharepoint/delete_list_item.ts | 2 +-
apps/sim/tools/sharepoint/delete_page.ts | 2 +-
apps/sim/tools/sharepoint/get_list.ts | 7 ++++---
apps/sim/tools/sharepoint/get_list_item.ts | 2 +-
apps/sim/tools/sharepoint/list_sites.ts | 4 ++--
apps/sim/tools/sharepoint/publish_page.ts | 2 +-
apps/sim/tools/sharepoint/update_list.ts | 2 +-
apps/sim/tools/sharepoint/update_page.ts | 4 ++--
11 files changed, 17 insertions(+), 16 deletions(-)
diff --git a/apps/sim/tools/sharepoint/add_list_items.ts b/apps/sim/tools/sharepoint/add_list_items.ts
index 1958a0fe9e6..c335c996cda 100644
--- a/apps/sim/tools/sharepoint/add_list_items.ts
+++ b/apps/sim/tools/sharepoint/add_list_items.ts
@@ -77,7 +77,7 @@ export const addListItemTool: ToolConfig ({
diff --git a/apps/sim/tools/sharepoint/create_list.ts b/apps/sim/tools/sharepoint/create_list.ts
index 1fb20f65180..d5640189be1 100644
--- a/apps/sim/tools/sharepoint/create_list.ts
+++ b/apps/sim/tools/sharepoint/create_list.ts
@@ -71,7 +71,7 @@ export const createListTool: ToolConfig {
const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
- return `https://graph.microsoft.com/v1.0/sites/${siteId}/lists`
+ return `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/lists`
},
method: 'POST',
headers: (params) => ({
diff --git a/apps/sim/tools/sharepoint/create_page.ts b/apps/sim/tools/sharepoint/create_page.ts
index 32a1e169269..4a56fa179fe 100644
--- a/apps/sim/tools/sharepoint/create_page.ts
+++ b/apps/sim/tools/sharepoint/create_page.ts
@@ -61,8 +61,8 @@ export const createPageTool: ToolConfig {
- const siteId = optionalTrim(params.siteSelector) || optionalTrim(params.siteId) || 'root'
- return `https://graph.microsoft.com/v1.0/sites/${siteId}/pages`
+ const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
+ return `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/pages`
},
method: 'POST',
headers: (params) => ({
diff --git a/apps/sim/tools/sharepoint/delete_list_item.ts b/apps/sim/tools/sharepoint/delete_list_item.ts
index b71e6b4e823..3c0c7dbb4ff 100644
--- a/apps/sim/tools/sharepoint/delete_list_item.ts
+++ b/apps/sim/tools/sharepoint/delete_list_item.ts
@@ -62,7 +62,7 @@ export const deleteListItemTool: ToolConfig<
if (!itemId) throw new Error('itemId must be provided')
const listSegment = encodeURIComponent(listId)
const itemSegment = encodeURIComponent(itemId)
- return `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${listSegment}/items/${itemSegment}`
+ return `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/lists/${listSegment}/items/${itemSegment}`
},
method: 'DELETE',
headers: (params) => ({
diff --git a/apps/sim/tools/sharepoint/delete_page.ts b/apps/sim/tools/sharepoint/delete_page.ts
index 4b5b77872eb..6a061951cb7 100644
--- a/apps/sim/tools/sharepoint/delete_page.ts
+++ b/apps/sim/tools/sharepoint/delete_page.ts
@@ -46,7 +46,7 @@ export const deletePageTool: ToolConfig ({
diff --git a/apps/sim/tools/sharepoint/get_list.ts b/apps/sim/tools/sharepoint/get_list.ts
index 5b1d4d44a21..a4061e32f57 100644
--- a/apps/sim/tools/sharepoint/get_list.ts
+++ b/apps/sim/tools/sharepoint/get_list.ts
@@ -73,10 +73,11 @@ export const getListTool: ToolConfig ({
diff --git a/apps/sim/tools/sharepoint/update_list.ts b/apps/sim/tools/sharepoint/update_list.ts
index 9fe9e1a69c3..850c97f8f03 100644
--- a/apps/sim/tools/sharepoint/update_list.ts
+++ b/apps/sim/tools/sharepoint/update_list.ts
@@ -69,7 +69,7 @@ export const updateListItemTool: ToolConfig<
throw new Error('listId must be provided')
}
const listSegment = encodeURIComponent(listId)
- return `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${listSegment}/items/${encodeURIComponent(itemId)}/fields`
+ return `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/lists/${listSegment}/items/${encodeURIComponent(itemId)}/fields`
},
method: 'PATCH',
headers: (params) => ({
diff --git a/apps/sim/tools/sharepoint/update_page.ts b/apps/sim/tools/sharepoint/update_page.ts
index 7c08d9b7dae..58cbf095552 100644
--- a/apps/sim/tools/sharepoint/update_page.ts
+++ b/apps/sim/tools/sharepoint/update_page.ts
@@ -63,10 +63,10 @@ export const updatePageTool: ToolConfig {
- const siteId = optionalTrim(params.siteSelector) || optionalTrim(params.siteId) || 'root'
+ const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
const pageId = optionalTrim(params.pageId)
if (!pageId) throw new Error('pageId must be provided')
- return `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/${encodeURIComponent(pageId)}/microsoft.graph.sitePage`
+ return `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/pages/${encodeURIComponent(pageId)}/microsoft.graph.sitePage`
},
method: 'PATCH',
headers: (params) => ({
From fe7b493bbc977537bd149939b36417c8b678a983 Mon Sep 17 00:00:00 2001
From: waleed
Date: Thu, 2 Jul 2026 00:34:33 -0700
Subject: [PATCH 3/6] fix(sharepoint): escape HTML in page content and stop
fallback from throwing
- create_page/update_page only escaped quotes when building innerHtml;
add a shared escapeHtml() helper that also escapes &, <, > so page
content with angle brackets/ampersands doesn't corrupt the canvas layout
- add_list_items' fields fallback (sanitized re-derivation when Graph's
response omits fields) could throw on a malformed fallback input after
the item was already created successfully; catch and fall back to
undefined instead of failing an already-successful response
---
apps/sim/tools/sharepoint/add_list_items.ts | 11 ++++++++---
apps/sim/tools/sharepoint/create_page.ts | 4 ++--
apps/sim/tools/sharepoint/update_page.ts | 4 ++--
apps/sim/tools/sharepoint/utils.ts | 9 +++++++++
4 files changed, 21 insertions(+), 7 deletions(-)
diff --git a/apps/sim/tools/sharepoint/add_list_items.ts b/apps/sim/tools/sharepoint/add_list_items.ts
index c335c996cda..3bfcd4836b9 100644
--- a/apps/sim/tools/sharepoint/add_list_items.ts
+++ b/apps/sim/tools/sharepoint/add_list_items.ts
@@ -99,9 +99,14 @@ export const addListItemTool: ToolConfig | undefined) ||
- (params ? resolveSanitizedFields(params.listItemFields) : undefined)
+ let fields = data?.fields as Record | undefined
+ if (!fields && params) {
+ try {
+ fields = resolveSanitizedFields(params.listItemFields)
+ } catch {
+ // Item was already created successfully; a malformed fallback input must not fail the response.
+ }
+ }
return {
success: true,
diff --git a/apps/sim/tools/sharepoint/create_page.ts b/apps/sim/tools/sharepoint/create_page.ts
index 4a56fa179fe..79453994c04 100644
--- a/apps/sim/tools/sharepoint/create_page.ts
+++ b/apps/sim/tools/sharepoint/create_page.ts
@@ -4,7 +4,7 @@ import type {
SharepointPage,
SharepointToolParams,
} from '@/tools/sharepoint/types'
-import { optionalTrim } from '@/tools/sharepoint/utils'
+import { escapeHtml, optionalTrim } from '@/tools/sharepoint/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SharePointCreatePage')
@@ -103,7 +103,7 @@ export const createPageTool: ToolConfig${pageContent.replace(/"/g, '"').replace(/'/g, ''')}
`,
+ innerHtml: `${escapeHtml(pageContent)}
`,
},
],
},
diff --git a/apps/sim/tools/sharepoint/update_page.ts b/apps/sim/tools/sharepoint/update_page.ts
index 58cbf095552..52f5da6153b 100644
--- a/apps/sim/tools/sharepoint/update_page.ts
+++ b/apps/sim/tools/sharepoint/update_page.ts
@@ -4,7 +4,7 @@ import type {
SharepointToolParams,
SharepointUpdatePageResponse,
} from '@/tools/sharepoint/types'
-import { optionalTrim } from '@/tools/sharepoint/utils'
+import { escapeHtml, optionalTrim } from '@/tools/sharepoint/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SharePointUpdatePage')
@@ -105,7 +105,7 @@ export const updatePageTool: ToolConfig${pageContent.replace(/"/g, '"').replace(/'/g, ''')}`,
+ innerHtml: `${escapeHtml(pageContent)}
`,
},
],
},
diff --git a/apps/sim/tools/sharepoint/utils.ts b/apps/sim/tools/sharepoint/utils.ts
index 353843f370e..9e7d60abacd 100644
--- a/apps/sim/tools/sharepoint/utils.ts
+++ b/apps/sim/tools/sharepoint/utils.ts
@@ -13,6 +13,15 @@ export function escapeODataString(value: string): string {
return value.replace(/'/g, "''")
}
+export function escapeHtml(value: string): string {
+ return value
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''')
+}
+
export function getGraphNextPageUrl(data: object): string | undefined {
const nextLink = (data as Record)['@odata.nextLink']
return typeof nextLink === 'string' ? nextLink : undefined
From 50ed83cff26b7b5ddf0b548444f8e4b00c09d988 Mon Sep 17 00:00:00 2001
From: waleed
Date: Thu, 2 Jul 2026 00:41:52 -0700
Subject: [PATCH 4/6] fix(sharepoint): scope columnDefinitions->pageContent
mapping to create_list
Both V1 and V2 tools.config.params mapped columnDefinitions onto pageContent
whenever columnDefinitions was truthy, with no operation check. Stale
list-column JSON left in block state after switching to create_page/
update_page would silently replace the user's intended page text. Gate
the mapping on operation === create_list (V1) / sharepoint_create_list (V2).
---
apps/sim/blocks/blocks/sharepoint.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts
index 84b1ba230fd..ed61efdf9b1 100644
--- a/apps/sim/blocks/blocks/sharepoint.ts
+++ b/apps/sim/blocks/blocks/sharepoint.ts
@@ -588,7 +588,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
baseParams.files = normalizedFiles
}
- if (columnDefinitions) {
+ if (columnDefinitions && others.operation === 'create_list') {
baseParams.pageContent = columnDefinitions
}
@@ -1154,7 +1154,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
maxPages: maxPages ? Number.parseInt(String(maxPages), 10) : undefined,
}
- if (columnDefinitions) {
+ if (columnDefinitions && rest.operation === 'sharepoint_create_list') {
result.pageContent = columnDefinitions
}
if (normalizedFiles) {
From 52793b2f911baec78fd47b6ba4bea8b57db422b5 Mon Sep 17 00:00:00 2001
From: waleed
Date: Thu, 2 Jul 2026 01:09:35 -0700
Subject: [PATCH 5/6] fix(sharepoint): cap download-file content fetch at
MAX_FILE_SIZE
Greptile flagged the content fetch as unbounded - a large file would
buffer entirely in memory before base64-encoding into the JSON response.
Pass maxResponseBytes: MAX_FILE_SIZE (100MB, same constant used by the
upload path) to secureFetchWithPinnedIP so oversized files reject early
with a clear PayloadSizeLimitError instead of exhausting memory.
---
apps/sim/app/api/tools/sharepoint/download-file/route.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/apps/sim/app/api/tools/sharepoint/download-file/route.ts b/apps/sim/app/api/tools/sharepoint/download-file/route.ts
index 6f3bb947770..1d9adcd5552 100644
--- a/apps/sim/app/api/tools/sharepoint/download-file/route.ts
+++ b/apps/sim/app/api/tools/sharepoint/download-file/route.ts
@@ -10,6 +10,7 @@ import {
} from '@/lib/core/security/input-validation.server'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
+import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation'
export const dynamic = 'force-dynamic'
@@ -122,6 +123,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
headers: { Authorization: authHeader },
// The content endpoint 302s to a preauthenticated URL on a different origin that needs no auth.
stripAuthOnRedirect: true,
+ maxResponseBytes: MAX_FILE_SIZE,
}
)
From 5a076d509ce123926ecf7d585d7bc9bc162110ee Mon Sep 17 00:00:00 2001
From: waleed
Date: Thu, 2 Jul 2026 08:31:35 -0700
Subject: [PATCH 6/6] fix(sharepoint): close V1 block input/output gaps and
encode upload URLs
Final validation pass (4 parallel audit agents against live Graph docs)
surfaced remaining gaps:
- apps/sim/app/api/tools/sharepoint/upload/route.ts: siteId/driveId were
not encodeURIComponent-ed in the Graph upload URL, unlike every other
sharepoint route/tool - same encoding-gap class fixed everywhere else
- read_page.ts: transformResponse recomputed siteId with a raw `||` in
3 spots instead of the optionalTrim(...) || optionalTrim(...) || 'root'
precedence used by request.url and every sibling tool
- SharepointBlock (V1, legacy/hidden-from-toolbar but still executable
for existing saved workflows): listItemFields and listItemId were
missing `required` for update_list despite the tool requiring them;
maxPages/groupId/includeColumns/includeItems/nextPageUrl subBlocks
were entirely absent, making those tool params unreachable from the
UI; block-level outputs were missing site/pages/content/totalPages/
nextPageUrl/lists/skippedFiles/skippedCount/errors even though the
underlying tools return them - V2 already covered all of these
---
.../app/api/tools/sharepoint/upload/route.ts | 4 +-
apps/sim/blocks/blocks/sharepoint.ts | 98 ++++++++++++++++++-
apps/sim/tools/sharepoint/read_page.ts | 6 +-
3 files changed, 102 insertions(+), 6 deletions(-)
diff --git a/apps/sim/app/api/tools/sharepoint/upload/route.ts b/apps/sim/app/api/tools/sharepoint/upload/route.ts
index 3c85a7bd7d1..55bb4eec935 100644
--- a/apps/sim/app/api/tools/sharepoint/upload/route.ts
+++ b/apps/sim/app/api/tools/sharepoint/upload/route.ts
@@ -135,8 +135,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
.join('/')
const uploadUrl = driveId
- ? `https://graph.microsoft.com/v1.0/drives/${driveId}/root:${encodedPath}:/content`
- : `https://graph.microsoft.com/v1.0/sites/${siteId}/drive/root:${encodedPath}:/content`
+ ? `https://graph.microsoft.com/v1.0/drives/${encodeURIComponent(driveId)}/root:${encodedPath}:/content`
+ : `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/drive/root:${encodedPath}:/content`
logger.info(`[${requestId}] Uploading to: ${uploadUrl}`)
diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts
index ed61efdf9b1..d73eb1adf09 100644
--- a/apps/sim/blocks/blocks/sharepoint.ts
+++ b/apps/sim/blocks/blocks/sharepoint.ts
@@ -183,7 +183,7 @@ export const SharepointBlock: BlockConfig = {
field: 'operation',
value: ['update_list', 'get_list_item', 'delete_list_item'],
},
- required: { field: 'operation', value: ['get_list_item', 'delete_list_item'] },
+ required: { field: 'operation', value: ['update_list', 'get_list_item', 'delete_list_item'] },
},
{
@@ -348,6 +348,7 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
'Enter list item fields as JSON (e.g., {"Title": "My Item", "Status": "Active"})',
canonicalParamId: 'listItemFields',
condition: { field: 'operation', value: ['update_list', 'add_list_items'] },
+ required: { field: 'operation', value: ['update_list', 'add_list_items'] },
wandConfig: {
enabled: true,
prompt: `Generate a JSON object for SharePoint list item fields based on the user's description.
@@ -390,6 +391,58 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
},
},
+ {
+ id: 'maxPages',
+ title: 'Max Pages',
+ type: 'short-input',
+ placeholder: 'Default 10, maximum 50',
+ condition: { field: 'operation', value: 'read_page' },
+ mode: 'advanced',
+ },
+ {
+ id: 'groupId',
+ title: 'Group ID',
+ type: 'short-input',
+ placeholder: 'Optional Microsoft 365 group ID',
+ condition: { field: 'operation', value: 'list_sites' },
+ mode: 'advanced',
+ },
+ {
+ id: 'includeColumns',
+ title: 'Include Columns',
+ type: 'dropdown',
+ options: [
+ { label: 'No', id: 'false' },
+ { label: 'Yes', id: 'true' },
+ ],
+ value: () => 'false',
+ condition: { field: 'operation', value: 'read_list' },
+ mode: 'advanced',
+ },
+ {
+ id: 'includeItems',
+ title: 'Include Items',
+ type: 'dropdown',
+ options: [
+ { label: 'Yes', id: 'true' },
+ { label: 'No', id: 'false' },
+ ],
+ value: () => 'true',
+ condition: { field: 'operation', value: 'read_list' },
+ mode: 'advanced',
+ },
+ {
+ id: 'nextPageUrl',
+ title: 'Next Page URL',
+ type: 'short-input',
+ placeholder: 'Paste the @odata.nextLink URL from a previous result',
+ condition: {
+ field: 'operation',
+ value: ['read_page', 'list_sites', 'read_list'],
+ },
+ mode: 'advanced',
+ },
+
// Upload / Download / Delete File / Get Drive Item operation fields
{
id: 'driveId',
@@ -624,6 +677,12 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
folderPath: { type: 'string', description: 'Folder path for file upload' },
fileName: { type: 'string', description: 'File name override' },
files: { type: 'array', description: 'Files to upload' },
+ maxPages: { type: 'number', description: 'Maximum pages to return when reading all pages' },
+ groupId: { type: 'string', description: 'Microsoft 365 group ID for group-owned sites' },
+ nextPageUrl: {
+ type: 'string',
+ description: 'Full Microsoft Graph @odata.nextLink URL from a previous result',
+ },
},
outputs: {
sites: {
@@ -631,16 +690,40 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
description:
'An array of SharePoint site objects, each containing details such as id, name, and more.',
},
+ site: {
+ type: 'json',
+ description: 'Single SharePoint site object (id, name, displayName, webUrl)',
+ },
page: {
type: 'json',
description: 'SharePoint page object (id, name, title, webUrl, pageLayout)',
},
+ pages: {
+ type: 'json',
+ description: 'Array of SharePoint pages with content ([{page, content}])',
+ },
+ content: {
+ type: 'json',
+ description: 'Content of the SharePoint page (content, canvasLayout)',
+ },
+ totalPages: {
+ type: 'number',
+ description: 'Total number of pages found when listing all pages',
+ },
+ nextPageUrl: {
+ type: 'string',
+ description: 'Full Microsoft Graph @odata.nextLink URL for the next page of results',
+ },
published: { type: 'boolean', description: 'Whether the page was published' },
deleted: { type: 'boolean', description: 'Whether the item/page/file was deleted' },
list: {
type: 'json',
description: 'SharePoint list object (id, displayName, name, webUrl, etc.)',
},
+ lists: {
+ type: 'json',
+ description: 'Array of SharePoint list objects when no specific list is requested',
+ },
item: {
type: 'json',
description: 'SharePoint list item with fields',
@@ -665,6 +748,19 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
type: 'number',
description: 'Number of files uploaded',
},
+ skippedFiles: {
+ type: 'json',
+ description:
+ 'Files skipped during upload for exceeding the size limit (name, size, limit, reason)',
+ },
+ skippedCount: {
+ type: 'number',
+ description: 'Number of files skipped during upload',
+ },
+ errors: {
+ type: 'json',
+ description: 'Per-file upload errors ([{name, error, status}])',
+ },
itemId: { type: 'string', description: 'ID of the deleted list item or file' },
pageId: { type: 'string', description: 'ID of the deleted or published page' },
success: {
diff --git a/apps/sim/tools/sharepoint/read_page.ts b/apps/sim/tools/sharepoint/read_page.ts
index 926ba9a1c48..8785b21a5da 100644
--- a/apps/sim/tools/sharepoint/read_page.ts
+++ b/apps/sim/tools/sharepoint/read_page.ts
@@ -181,7 +181,7 @@ export const readPageTool: ToolConfig