fix(microsoft-planner): align with live Graph API docs, add plan CRUD + categories (#5486)

* fix(microsoft-planner): align with live Graph API docs, add plan CRUD + categories

- fix update_task never returning updated task (missing Prefer: return=representation)
- fix wrong @odata.type on task assignments (missing leading #)
- fix update_plan/update_plan_details/update_bucket/update_task_details crashing
  on 204 No Content responses to PATCH (Graph sometimes ignores Prefer header)
- add .trim() on path IDs across tools invoked outside the block
- add create_plan, update_plan, delete_plan, get_plan_details, update_plan_details
  tools (If-Match/etag handled correctly on update/delete)
- add appliedCategories support on create_task/update_task
- all new tools verified against Graph API Permissions tables to require only
  scopes we already request (Group.ReadWrite.All, Group.Read.All, Tasks.ReadWrite)
- regenerate integration docs

* fix(microsoft-planner): use Graph-specific error extractor consistently

7 tools were pinned to the generic 'nested-error-object' extractor
instead of MICROSOFT_GRAPH_ERRORS, losing Graph's inner-error detail
(e.g. ETag mismatch specifics) that sibling Microsoft integrations
(Excel, OneDrive, SharePoint) already surface correctly.

* fix(microsoft-planner): address Cursor/Greptile round-1 review findings

- fix empty priority/percentComplete string coercing to 0 (Number('')) at
  the block layer, which Planner treats as urgent/0% instead of leaving unset
- support removing applied categories on update via a "-category3" prefix,
  since Graph only clears a label when its key is explicitly set to false
- add missing MICROSOFT_GRAPH_ERRORS extractor to delete_plan/get_plan_details

* fix(microsoft-planner): don't return a stale etag on 204 update_task response

Graph's If-Match update changes the resource's etag even when it returns
204 No Content instead of the updated representation. Returning the
request's (now-stale) etag as if it were current would let a chained
update silently send a wrong If-Match and fail with 412. Return an empty
etag instead so update_task's own etag-required guard forces a re-fetch.
This commit is contained in:
Waleed
2026-07-07 12:14:38 -07:00
committed by GitHub
parent 98dc1eea39
commit 1a87f1a83c
21 changed files with 1244 additions and 63 deletions
@@ -64,10 +64,13 @@ Create a new task in Microsoft Planner
| --------- | ---- | -------- | ----------- |
| `planId` | string | Yes | The ID of the plan where the task will be created \(e.g., "xqQg5FS2LkCe54tAMV_v2ZgADW2J"\) |
| `title` | string | Yes | The title of the task \(e.g., "Review quarterly report"\) |
| `description` | string | No | The description of the task |
| `dueDateTime` | string | No | The due date and time for the task in ISO 8601 format \(e.g., "2025-03-15T17:00:00Z"\) |
| `startDateTime` | string | No | The start date and time for the task in ISO 8601 format \(e.g., "2025-03-10T09:00:00Z"\) |
| `priority` | number | No | The priority of the task \(0-10, where 0 is urgent and 10 is low\) |
| `percentComplete` | number | No | The percentage of task completion \(0-100\) |
| `assigneeUserId` | string | No | The user ID to assign the task to \(e.g., "e82f74c3-4d8a-4b5c-9f1e-2a6b8c9d0e3f"\) |
| `bucketId` | string | No | The bucket ID to place the task in \(e.g., "hsOf2dhOJkC6Fey9VjDg1JgAC9Rq"\) |
| `appliedCategories` | string | No | Comma-separated category labels to apply to the task, e.g. "category1,category3" \(up to category1-category25, plan-defined color labels\) |
#### Output
@@ -97,6 +100,7 @@ Update a task in Microsoft Planner
| `percentComplete` | number | No | The percentage of task completion \(0-100\) |
| `priority` | number | No | The priority of the task \(0-10\) |
| `assigneeUserId` | string | No | The user ID to assign the task to \(e.g., "e82f74c3-4d8a-4b5c-9f1e-2a6b8c9d0e3f"\) |
| `appliedCategories` | string | No | Comma-separated category labels to apply to the task, e.g. "category1,category3" \(up to category1-category25, plan-defined color labels\) |
#### Output
@@ -170,6 +174,109 @@ Get details of a specific Microsoft Planner plan
| ↳ `planId` | string | Plan ID |
| ↳ `planUrl` | string | Microsoft Graph API URL for the plan |
### `microsoft_planner_create_plan`
Create a new Microsoft Planner plan owned by a Microsoft 365 group
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `groupId` | string | Yes | The ID of the Microsoft 365 group that will own the plan \(e.g., "ebf3b108-5234-4e22-b93d-656d7dae5874"\). The current user must be a member of this group. |
| `title` | string | Yes | The title of the plan |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the plan was created successfully |
| `plan` | object | The created plan object with all properties |
| `metadata` | object | Metadata including planId and groupId |
| ↳ `planId` | string | Created plan ID |
| ↳ `groupId` | string | Owning Microsoft 365 group ID |
### `microsoft_planner_update_plan`
Rename a Microsoft Planner plan
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `planId` | string | Yes | The ID of the plan to update \(e.g., "xqQg5FS2LkCe54tAMV_v2ZgADW2J"\) |
| `etag` | string | Yes | The ETag value from the plan to update \(If-Match header\) |
| `title` | string | Yes | The new title of the plan |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the plan was updated successfully |
| `plan` | object | The updated plan object with all properties |
| `metadata` | object | Metadata including planId |
| ↳ `planId` | string | Updated plan ID |
### `microsoft_planner_get_plan_details`
Get detailed information about a plan including category descriptions and sharing
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `planId` | string | Yes | The ID of the plan \(e.g., "xqQg5FS2LkCe54tAMV_v2ZgADW2J"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the plan details were retrieved successfully |
| `planDetails` | object | The plan details including categoryDescriptions and sharedWith |
| `etag` | string | The ETag value for this plan details resource |
| `metadata` | object | Metadata including planId |
| ↳ `planId` | string | Plan ID |
### `microsoft_planner_update_plan_details`
Update a plan's category (color label) descriptions and shared-with user list in Microsoft Planner
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `planId` | string | Yes | The ID of the plan \(e.g., "xqQg5FS2LkCe54tAMV_v2ZgADW2J"\) |
| `etag` | string | Yes | The ETag value from the plan details to update \(If-Match header\) |
| `categoryDescriptions` | object | No | Category label names as a JSON object, e.g. \{"category1": "Blocked", "category2": "At Risk"\}. Set a value to null to clear a label. |
| `sharedWith` | object | No | User IDs to share the plan with as a JSON object, e.g. \{"<user-id>": true\}. Set a value to false to unshare. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the plan details were updated successfully |
| `planDetails` | object | The updated plan details object with categoryDescriptions and sharedWith |
| `metadata` | object | Metadata including planId |
| ↳ `planId` | string | Plan ID |
### `microsoft_planner_delete_plan`
Delete a Microsoft Planner plan
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `planId` | string | Yes | The ID of the plan to delete \(e.g., "xqQg5FS2LkCe54tAMV_v2ZgADW2J"\) |
| `etag` | string | Yes | The ETag value from the plan to delete \(If-Match header\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the plan was deleted successfully |
| `deleted` | boolean | Confirmation of deletion |
| `metadata` | object | Additional metadata |
### `microsoft_planner_list_buckets`
List all buckets in a Microsoft Planner plan
+235 -17
View File
@@ -23,6 +23,9 @@ interface MicrosoftPlannerBlockParams {
checklist?: string
references?: string
previewType?: string
appliedCategories?: string
categoryDescriptions?: string
sharedWith?: string
[key: string]: string | number | boolean | undefined
}
@@ -50,6 +53,11 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
{ label: 'Delete Task', id: 'delete_task' },
{ label: 'List Plans', id: 'list_plans' },
{ label: 'Read Plan', id: 'read_plan' },
{ label: 'Create Plan', id: 'create_plan' },
{ label: 'Update Plan', id: 'update_plan' },
{ label: 'Get Plan Details', id: 'get_plan_details' },
{ label: 'Update Plan Details', id: 'update_plan_details' },
{ label: 'Delete Plan', id: 'delete_plan' },
{ label: 'List Buckets', id: 'list_buckets' },
{ label: 'Read Bucket', id: 'read_bucket' },
{ label: 'Create Bucket', id: 'create_bucket' },
@@ -91,11 +99,30 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
mode: 'basic',
condition: {
field: 'operation',
value: ['create_task', 'read_task', 'read_plan', 'list_buckets', 'create_bucket'],
value: [
'create_task',
'read_task',
'read_plan',
'list_buckets',
'create_bucket',
'update_plan',
'delete_plan',
'get_plan_details',
'update_plan_details',
],
},
required: {
field: 'operation',
value: ['read_plan', 'list_buckets', 'create_bucket', 'create_task'],
value: [
'read_plan',
'list_buckets',
'create_bucket',
'create_task',
'update_plan',
'delete_plan',
'get_plan_details',
'update_plan_details',
],
},
},
@@ -109,11 +136,30 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
mode: 'advanced',
condition: {
field: 'operation',
value: ['create_task', 'read_task', 'read_plan', 'list_buckets', 'create_bucket'],
value: [
'create_task',
'read_task',
'read_plan',
'list_buckets',
'create_bucket',
'update_plan',
'delete_plan',
'get_plan_details',
'update_plan_details',
],
},
required: {
field: 'operation',
value: ['read_plan', 'list_buckets', 'create_bucket', 'create_task'],
value: [
'read_plan',
'list_buckets',
'create_bucket',
'create_task',
'update_plan',
'delete_plan',
'get_plan_details',
'update_plan_details',
],
},
dependsOn: ['credential'],
},
@@ -184,6 +230,9 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
'update_bucket',
'delete_bucket',
'update_task_details',
'update_plan',
'update_plan_details',
'delete_plan',
],
},
dependsOn: ['credential'],
@@ -209,13 +258,14 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
required: { field: 'operation', value: 'create_bucket' },
},
// Description for task details
// Description for task details (Microsoft Planner tasks store description on the
// task details resource, not the task itself, so this only applies to update_task_details)
{
id: 'description',
title: 'Description',
type: 'long-input',
placeholder: 'Enter task description',
condition: { field: 'operation', value: ['create_task', 'update_task_details'] },
condition: { field: 'operation', value: ['update_task_details'] },
},
// Due Date
@@ -247,7 +297,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
title: 'Start Date',
type: 'short-input',
placeholder: 'Enter start date in ISO 8601 format (optional)',
condition: { field: 'operation', value: ['update_task'] },
mode: 'advanced',
condition: { field: 'operation', value: ['create_task', 'update_task'] },
wandConfig: {
enabled: true,
prompt: `Generate an ISO 8601 timestamp based on the user's description for Microsoft Planner task start date.
@@ -288,7 +339,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
title: 'Priority',
type: 'short-input',
placeholder: 'Enter priority (0-10, optional)',
condition: { field: 'operation', value: ['update_task'] },
mode: 'advanced',
condition: { field: 'operation', value: ['create_task', 'update_task'] },
},
// Percent Complete
@@ -297,7 +349,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
title: 'Percent Complete',
type: 'short-input',
placeholder: 'Enter completion percentage (0-100, optional)',
condition: { field: 'operation', value: ['update_task'] },
mode: 'advanced',
condition: { field: 'operation', value: ['create_task', 'update_task'] },
},
// Checklist for task details
@@ -305,7 +358,9 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
id: 'checklist',
title: 'Checklist (JSON)',
type: 'long-input',
placeholder: 'Enter checklist as JSON object (optional)',
placeholder:
'e.g. {"<generated-guid>": {"@odata.type": "microsoft.graph.plannerChecklistItem", "title": "Step 1", "isChecked": false}}',
mode: 'advanced',
condition: { field: 'operation', value: ['update_task_details'] },
},
@@ -314,7 +369,9 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
id: 'references',
title: 'References (JSON)',
type: 'long-input',
placeholder: 'Enter references as JSON object (optional)',
placeholder:
'e.g. {"https%3A//example%2Ecom": {"@odata.type": "microsoft.graph.plannerExternalReference", "alias": "Docs", "type": "Other"}}',
mode: 'advanced',
condition: { field: 'operation', value: ['update_task_details'] },
},
@@ -324,8 +381,74 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
title: 'Preview Type',
type: 'short-input',
placeholder: 'Enter preview type (automatic, noPreview, checklist, description, reference)',
mode: 'advanced',
condition: { field: 'operation', value: ['update_task_details'] },
},
// Group ID for create plan
{
id: 'groupId',
title: 'Microsoft 365 Group ID',
type: 'short-input',
placeholder: 'Enter the Microsoft 365 group ID that will own the plan',
required: { field: 'operation', value: 'create_plan' },
condition: { field: 'operation', value: ['create_plan'] },
dependsOn: ['credential'],
},
// Plan title for create/update plan
{
id: 'planTitle',
title: 'Plan Title',
type: 'short-input',
placeholder: 'Enter the plan title',
required: { field: 'operation', value: ['create_plan', 'update_plan'] },
condition: { field: 'operation', value: ['create_plan', 'update_plan'] },
dependsOn: ['credential'],
},
// Applied categories for task create/update (color labels)
{
id: 'appliedCategories',
title: 'Categories',
type: 'short-input',
placeholder: 'e.g. category1,category3,-category5',
mode: 'advanced',
condition: { field: 'operation', value: ['create_task', 'update_task'] },
wandConfig: {
enabled: true,
prompt: `Generate a comma-separated list of Microsoft Planner category keys based on the user's description.
Valid keys are category1 through category25. Prefix a key with "-" to remove/clear that label (only meaningful on update).
Examples:
- "flag it blocked" -> category1
- "mark as urgent and needs review" -> category1,category2
- "remove the blocked label" -> -category1
Return ONLY the comma-separated category keys - no explanations, no extra text.`,
placeholder:
'Describe which category labels to apply or remove (e.g., "mark as blocked", "remove urgent label")...',
},
},
// Category descriptions for plan details (color label names)
{
id: 'categoryDescriptions',
title: 'Category Descriptions (JSON)',
type: 'long-input',
placeholder: 'e.g. {"category1": "Blocked", "category2": "At Risk"}',
mode: 'advanced',
condition: { field: 'operation', value: ['update_plan_details'] },
},
// Shared with for plan details
{
id: 'sharedWith',
title: 'Shared With (JSON)',
type: 'long-input',
placeholder: 'e.g. {"<user-id>": true}',
mode: 'advanced',
condition: { field: 'operation', value: ['update_plan_details'] },
},
],
tools: {
access: [
@@ -335,6 +458,11 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
'microsoft_planner_delete_task',
'microsoft_planner_list_plans',
'microsoft_planner_read_plan',
'microsoft_planner_create_plan',
'microsoft_planner_update_plan',
'microsoft_planner_get_plan_details',
'microsoft_planner_update_plan_details',
'microsoft_planner_delete_plan',
'microsoft_planner_list_buckets',
'microsoft_planner_read_bucket',
'microsoft_planner_create_bucket',
@@ -358,6 +486,16 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
return 'microsoft_planner_list_plans'
case 'read_plan':
return 'microsoft_planner_read_plan'
case 'create_plan':
return 'microsoft_planner_create_plan'
case 'update_plan':
return 'microsoft_planner_update_plan'
case 'get_plan_details':
return 'microsoft_planner_get_plan_details'
case 'update_plan_details':
return 'microsoft_planner_update_plan_details'
case 'delete_plan':
return 'microsoft_planner_delete_plan'
case 'list_buckets':
return 'microsoft_planner_list_buckets'
case 'read_bucket':
@@ -388,6 +526,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
bucketIdForRead,
title,
name,
planTitle,
description,
dueDateTime,
startDateTime,
@@ -398,6 +537,9 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
checklist,
references,
previewType,
appliedCategories,
categoryDescriptions,
sharedWith,
...rest
} = params
@@ -424,6 +566,58 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
}
}
// Create Plan
if (operation === 'create_plan') {
return {
...baseParams,
groupId: groupId?.trim(),
title: planTitle?.trim(),
}
}
// Update Plan
if (operation === 'update_plan') {
return {
...baseParams,
planId: planId?.trim(),
etag: etag?.trim(),
title: planTitle?.trim(),
}
}
// Get Plan Details
if (operation === 'get_plan_details') {
return {
...baseParams,
planId: planId?.trim(),
}
}
// Update Plan Details
if (operation === 'update_plan_details') {
const updatePlanDetailsParams: MicrosoftPlannerBlockParams = {
...baseParams,
planId: planId?.trim(),
etag: etag?.trim(),
}
if (categoryDescriptions?.trim()) {
updatePlanDetailsParams.categoryDescriptions = categoryDescriptions.trim()
}
if (sharedWith?.trim()) {
updatePlanDetailsParams.sharedWith = sharedWith.trim()
}
return updatePlanDetailsParams
}
// Delete Plan
if (operation === 'delete_plan') {
return {
...baseParams,
planId: planId?.trim(),
etag: etag?.trim(),
}
}
// List Buckets
if (operation === 'list_buckets') {
return {
@@ -492,18 +686,27 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
title: title?.trim(),
}
if (description?.trim()) {
createParams.description = description.trim()
}
if (dueDateTime?.trim()) {
createParams.dueDateTime = dueDateTime.trim()
}
if (startDateTime?.trim()) {
createParams.startDateTime = startDateTime.trim()
}
if (priority !== undefined && String(priority).trim() !== '') {
createParams.priority = Number(priority)
}
if (percentComplete !== undefined && String(percentComplete).trim() !== '') {
createParams.percentComplete = Number(percentComplete)
}
if (assigneeUserId?.trim()) {
createParams.assigneeUserId = assigneeUserId.trim()
}
if (effectiveBucketId) {
createParams.bucketId = effectiveBucketId
}
if (appliedCategories?.trim()) {
createParams.appliedCategories = appliedCategories.trim()
}
return createParams
}
@@ -531,12 +734,15 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
if (assigneeUserId?.trim()) {
updateParams.assigneeUserId = assigneeUserId.trim()
}
if (priority !== undefined) {
if (priority !== undefined && String(priority).trim() !== '') {
updateParams.priority = Number(priority)
}
if (percentComplete !== undefined) {
if (percentComplete !== undefined && String(percentComplete).trim() !== '') {
updateParams.percentComplete = Number(percentComplete)
}
if (appliedCategories?.trim()) {
updateParams.appliedCategories = appliedCategories.trim()
}
return updateParams
}
@@ -597,7 +803,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
bucketIdForRead: { type: 'string', description: 'Bucket ID for read operations' },
title: { type: 'string', description: 'Task title' },
name: { type: 'string', description: 'Bucket name' },
description: { type: 'string', description: 'Task or task details description' },
planTitle: { type: 'string', description: 'Plan title for create/update plan' },
description: { type: 'string', description: 'Task details description' },
dueDateTime: { type: 'string', description: 'Due date' },
startDateTime: { type: 'string', description: 'Start date' },
assigneeUserId: { type: 'string', description: 'Assignee user ID' },
@@ -607,6 +814,12 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
checklist: { type: 'string', description: 'Checklist items as JSON' },
references: { type: 'string', description: 'References as JSON' },
previewType: { type: 'string', description: 'Preview type for task details' },
appliedCategories: {
type: 'string',
description: 'Comma-separated category labels to apply to a task (e.g., category1,category3)',
},
categoryDescriptions: { type: 'string', description: 'Plan category label names as JSON' },
sharedWith: { type: 'string', description: 'Plan shared-with user IDs as JSON' },
},
outputs: {
message: {
@@ -638,6 +851,11 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
type: 'json',
description: 'Array of Microsoft Planner plans',
},
planDetails: {
type: 'json',
description:
'The Microsoft Planner plan details, including categoryDescriptions and sharedWith',
},
bucket: {
type: 'json',
description: 'The Microsoft Planner bucket object',
@@ -0,0 +1,112 @@
import { createLogger } from '@sim/logger'
import { ErrorExtractorId } from '@/tools/error-extractors'
import type {
MicrosoftPlannerCreatePlanResponse,
MicrosoftPlannerToolParams,
} from '@/tools/microsoft_planner/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('MicrosoftPlannerCreatePlan')
export const createPlanTool: ToolConfig<
MicrosoftPlannerToolParams,
MicrosoftPlannerCreatePlanResponse
> = {
id: 'microsoft_planner_create_plan',
name: 'Create Microsoft Planner Plan',
description: 'Create a new Microsoft Planner plan owned by a Microsoft 365 group',
version: '1.0',
errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS,
oauth: {
required: true,
provider: 'microsoft-planner',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Planner API',
},
groupId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the Microsoft 365 group that will own the plan (e.g., "ebf3b108-5234-4e22-b93d-656d7dae5874"). The current user must be a member of this group.',
},
title: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The title of the plan',
},
},
request: {
url: () => 'https://graph.microsoft.com/v1.0/planner/plans',
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
const groupId = params.groupId?.trim()
if (!groupId) {
throw new Error('Microsoft 365 group ID is required')
}
if (!params.title) {
throw new Error('Plan title is required')
}
const body = {
container: {
url: `https://graph.microsoft.com/v1.0/groups/${groupId}`,
},
title: params.title,
}
logger.info('Creating plan with body:', body)
return body
},
},
transformResponse: async (response: Response) => {
const plan = await response.json()
logger.info('Created plan:', plan)
const result: MicrosoftPlannerCreatePlanResponse = {
success: true,
output: {
plan,
metadata: {
planId: plan.id,
groupId: plan.container?.containerId,
},
},
}
return result
},
outputs: {
success: { type: 'boolean', description: 'Whether the plan was created successfully' },
plan: { type: 'object', description: 'The created plan object with all properties' },
metadata: {
type: 'object',
description: 'Metadata including planId and groupId',
properties: {
planId: { type: 'string', description: 'Created plan ID' },
groupId: { type: 'string', description: 'Owning Microsoft 365 group ID' },
},
},
},
}
@@ -42,12 +42,6 @@ export const createTaskTool: ToolConfig<
visibility: 'user-or-llm',
description: 'The title of the task (e.g., "Review quarterly report")',
},
description: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'The description of the task',
},
dueDateTime: {
type: 'string',
required: false,
@@ -55,6 +49,25 @@ export const createTaskTool: ToolConfig<
description:
'The due date and time for the task in ISO 8601 format (e.g., "2025-03-15T17:00:00Z")',
},
startDateTime: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'The start date and time for the task in ISO 8601 format (e.g., "2025-03-10T09:00:00Z")',
},
priority: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'The priority of the task (0-10, where 0 is urgent and 10 is low)',
},
percentComplete: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'The percentage of task completion (0-100)',
},
assigneeUserId: {
type: 'string',
required: false,
@@ -68,6 +81,13 @@ export const createTaskTool: ToolConfig<
visibility: 'user-or-llm',
description: 'The bucket ID to place the task in (e.g., "hsOf2dhOJkC6Fey9VjDg1JgAC9Rq")',
},
appliedCategories: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated category labels to apply to the task, e.g. "category1,category3" (up to category1-category25, plan-defined color labels)',
},
},
request: {
@@ -108,6 +128,22 @@ export const createTaskTool: ToolConfig<
body.dueDateTime = params.dueDateTime
}
if (
params.startDateTime !== undefined &&
params.startDateTime !== null &&
params.startDateTime !== ''
) {
body.startDateTime = params.startDateTime
}
if (params.priority !== undefined && params.priority !== null) {
body.priority = Number(params.priority)
}
if (params.percentComplete !== undefined && params.percentComplete !== null) {
body.percentComplete = Number(params.percentComplete)
}
if (
params.assigneeUserId !== undefined &&
params.assigneeUserId !== null &&
@@ -115,12 +151,27 @@ export const createTaskTool: ToolConfig<
) {
body.assignments = {
[params.assigneeUserId]: {
'@odata.type': 'microsoft.graph.plannerAssignment',
'@odata.type': '#microsoft.graph.plannerAssignment',
orderHint: ' !',
},
}
}
if (params.appliedCategories?.trim()) {
const categories = params.appliedCategories
.split(',')
.map((category) => category.trim())
.filter(Boolean)
if (categories.length > 0) {
body.appliedCategories = Object.fromEntries(
categories.map((category) =>
category.startsWith('-') ? [category.slice(1), false] : [category, true]
)
)
}
}
logger.info('Creating task with body:', body)
return body
},
@@ -44,10 +44,11 @@ export const deleteBucketTool: ToolConfig<
request: {
url: (params) => {
if (!params.bucketId) {
const bucketId = params.bucketId?.trim()
if (!bucketId) {
throw new Error('Bucket ID is required')
}
return `https://graph.microsoft.com/v1.0/planner/buckets/${params.bucketId}`
return `https://graph.microsoft.com/v1.0/planner/buckets/${bucketId}`
},
method: 'DELETE',
headers: (params) => {
@@ -0,0 +1,102 @@
import { createLogger } from '@sim/logger'
import { ErrorExtractorId } from '@/tools/error-extractors'
import type {
MicrosoftPlannerDeletePlanResponse,
MicrosoftPlannerToolParams,
} from '@/tools/microsoft_planner/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('MicrosoftPlannerDeletePlan')
export const deletePlanTool: ToolConfig<
MicrosoftPlannerToolParams,
MicrosoftPlannerDeletePlanResponse
> = {
id: 'microsoft_planner_delete_plan',
name: 'Delete Microsoft Planner Plan',
description: 'Delete a Microsoft Planner plan',
version: '1.0',
errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS,
oauth: {
required: true,
provider: 'microsoft-planner',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Planner API',
},
planId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the plan to delete (e.g., "xqQg5FS2LkCe54tAMV_v2ZgADW2J")',
},
etag: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The ETag value from the plan to delete (If-Match header)',
},
},
request: {
url: (params) => {
const planId = params.planId?.trim()
if (!planId) {
throw new Error('Plan ID is required')
}
return `https://graph.microsoft.com/v1.0/planner/plans/${planId}`
},
method: 'DELETE',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
if (!params.etag) {
throw new Error('ETag is required for delete operations')
}
let cleanedEtag = params.etag.trim()
while (cleanedEtag.startsWith('"') && cleanedEtag.endsWith('"')) {
cleanedEtag = cleanedEtag.slice(1, -1)
logger.info('Removed surrounding quotes:', cleanedEtag)
}
if (cleanedEtag.includes('\\"')) {
cleanedEtag = cleanedEtag.replace(/\\"/g, '"')
logger.info('Cleaned escaped quotes from etag')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'If-Match': cleanedEtag,
}
},
},
transformResponse: async (response: Response) => {
logger.info('Plan deleted successfully')
const result: MicrosoftPlannerDeletePlanResponse = {
success: true,
output: {
deleted: true,
metadata: {},
},
}
return result
},
outputs: {
success: { type: 'boolean', description: 'Whether the plan was deleted successfully' },
deleted: { type: 'boolean', description: 'Confirmation of deletion' },
metadata: { type: 'object', description: 'Additional metadata' },
},
}
@@ -44,10 +44,11 @@ export const deleteTaskTool: ToolConfig<
request: {
url: (params) => {
if (!params.taskId) {
const taskId = params.taskId?.trim()
if (!taskId) {
throw new Error('Task ID is required')
}
return `https://graph.microsoft.com/v1.0/planner/tasks/${params.taskId}`
return `https://graph.microsoft.com/v1.0/planner/tasks/${taskId}`
},
method: 'DELETE',
headers: (params) => {
@@ -0,0 +1,102 @@
import { createLogger } from '@sim/logger'
import { ErrorExtractorId } from '@/tools/error-extractors'
import type {
MicrosoftPlannerGetPlanDetailsResponse,
MicrosoftPlannerToolParams,
} from '@/tools/microsoft_planner/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('MicrosoftPlannerGetPlanDetails')
export const getPlanDetailsTool: ToolConfig<
MicrosoftPlannerToolParams,
MicrosoftPlannerGetPlanDetailsResponse
> = {
id: 'microsoft_planner_get_plan_details',
name: 'Get Microsoft Planner Plan Details',
description: 'Get detailed information about a plan including category descriptions and sharing',
version: '1.0',
errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS,
oauth: {
required: true,
provider: 'microsoft-planner',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Planner API',
},
planId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the plan (e.g., "xqQg5FS2LkCe54tAMV_v2ZgADW2J")',
},
},
request: {
url: (params) => {
const planId = params.planId?.trim()
if (!planId) {
throw new Error('Plan ID is required')
}
return `https://graph.microsoft.com/v1.0/planner/plans/${planId}/details`
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
},
transformResponse: async (response: Response) => {
const planDetails = await response.json()
logger.info('Plan details retrieved:', planDetails)
const etag = planDetails['@odata.etag'] || ''
const result: MicrosoftPlannerGetPlanDetailsResponse = {
success: true,
output: {
planDetails,
etag,
metadata: {
planId: planDetails.id,
},
},
}
return result
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the plan details were retrieved successfully',
},
planDetails: {
type: 'object',
description: 'The plan details including categoryDescriptions and sharedWith',
},
etag: {
type: 'string',
description: 'The ETag value for this plan details resource',
},
metadata: {
type: 'object',
description: 'Metadata including planId',
properties: {
planId: { type: 'string', description: 'Plan ID' },
},
},
},
}
@@ -38,10 +38,11 @@ export const getTaskDetailsTool: ToolConfig<
request: {
url: (params) => {
if (!params.taskId) {
const taskId = params.taskId?.trim()
if (!taskId) {
throw new Error('Task ID is required')
}
return `https://graph.microsoft.com/v1.0/planner/tasks/${params.taskId}/details`
return `https://graph.microsoft.com/v1.0/planner/tasks/${taskId}/details`
},
method: 'GET',
headers: (params) => {
+10
View File
@@ -1,7 +1,10 @@
import { createBucketTool } from '@/tools/microsoft_planner/create_bucket'
import { createPlanTool } from '@/tools/microsoft_planner/create_plan'
import { createTaskTool } from '@/tools/microsoft_planner/create_task'
import { deleteBucketTool } from '@/tools/microsoft_planner/delete_bucket'
import { deletePlanTool } from '@/tools/microsoft_planner/delete_plan'
import { deleteTaskTool } from '@/tools/microsoft_planner/delete_task'
import { getPlanDetailsTool } from '@/tools/microsoft_planner/get_plan_details'
import { getTaskDetailsTool } from '@/tools/microsoft_planner/get_task_details'
import { listBucketsTool } from '@/tools/microsoft_planner/list_buckets'
import { listPlansTool } from '@/tools/microsoft_planner/list_plans'
@@ -9,6 +12,8 @@ import { readBucketTool } from '@/tools/microsoft_planner/read_bucket'
import { readPlanTool } from '@/tools/microsoft_planner/read_plan'
import { readTaskTool } from '@/tools/microsoft_planner/read_task'
import { updateBucketTool } from '@/tools/microsoft_planner/update_bucket'
import { updatePlanTool } from '@/tools/microsoft_planner/update_plan'
import { updatePlanDetailsTool } from '@/tools/microsoft_planner/update_plan_details'
import { updateTaskTool } from '@/tools/microsoft_planner/update_task'
import { updateTaskDetailsTool } from '@/tools/microsoft_planner/update_task_details'
@@ -18,6 +23,11 @@ export const microsoftPlannerUpdateTaskTool = updateTaskTool
export const microsoftPlannerDeleteTaskTool = deleteTaskTool
export const microsoftPlannerListPlansTool = listPlansTool
export const microsoftPlannerReadPlanTool = readPlanTool
export const microsoftPlannerCreatePlanTool = createPlanTool
export const microsoftPlannerUpdatePlanTool = updatePlanTool
export const microsoftPlannerGetPlanDetailsTool = getPlanDetailsTool
export const microsoftPlannerUpdatePlanDetailsTool = updatePlanDetailsTool
export const microsoftPlannerDeletePlanTool = deletePlanTool
export const microsoftPlannerListBucketsTool = listBucketsTool
export const microsoftPlannerReadBucketTool = readBucketTool
export const microsoftPlannerCreateBucketTool = createBucketTool
@@ -38,10 +38,11 @@ export const listBucketsTool: ToolConfig<
request: {
url: (params) => {
if (!params.planId) {
const planId = params.planId?.trim()
if (!planId) {
throw new Error('Plan ID is required')
}
return `https://graph.microsoft.com/v1.0/planner/plans/${params.planId}/buckets`
return `https://graph.microsoft.com/v1.0/planner/plans/${planId}/buckets`
},
method: 'GET',
headers: (params) => {
@@ -38,10 +38,11 @@ export const readBucketTool: ToolConfig<
request: {
url: (params) => {
if (!params.bucketId) {
const bucketId = params.bucketId?.trim()
if (!bucketId) {
throw new Error('Bucket ID is required')
}
return `https://graph.microsoft.com/v1.0/planner/buckets/${params.bucketId}`
return `https://graph.microsoft.com/v1.0/planner/buckets/${bucketId}`
},
method: 'GET',
headers: (params) => {
@@ -38,10 +38,11 @@ export const readPlanTool: ToolConfig<
request: {
url: (params) => {
if (!params.planId) {
const planId = params.planId?.trim()
if (!planId) {
throw new Error('Plan ID is required')
}
return `https://graph.microsoft.com/v1.0/planner/plans/${params.planId}`
return `https://graph.microsoft.com/v1.0/planner/plans/${planId}`
},
method: 'GET',
headers: (params) => {
@@ -1,4 +1,5 @@
import { createLogger } from '@sim/logger'
import { ErrorExtractorId } from '@/tools/error-extractors'
import type {
MicrosoftPlannerReadResponse,
MicrosoftPlannerToolParams,
@@ -13,7 +14,7 @@ export const readTaskTool: ToolConfig<MicrosoftPlannerToolParams, MicrosoftPlann
description:
'Read tasks from Microsoft Planner - get all user tasks or all tasks from a specific plan',
version: '1.0',
errorExtractor: 'nested-error-object',
errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS,
oauth: {
required: true,
+59 -3
View File
@@ -41,6 +41,10 @@ interface PlannerContainer {
url?: string
}
interface PlannerAppliedCategories {
[category: string]: boolean
}
export interface PlannerTask {
id?: string
planId: string
@@ -61,6 +65,7 @@ export interface PlannerTask {
conversationThreadId?: string
priority?: number
assignments?: Record<string, PlannerAssignment>
appliedCategories?: PlannerAppliedCategories
bucketId?: string
details?: {
description?: string
@@ -69,7 +74,7 @@ export interface PlannerTask {
}
}
interface PlannerBucket {
export interface PlannerBucket {
id: string
name: string
planId: string
@@ -77,7 +82,7 @@ interface PlannerBucket {
'@odata.etag'?: string
}
interface PlannerPlan {
export interface PlannerPlan {
id: string
title: string
owner?: string
@@ -86,7 +91,14 @@ interface PlannerPlan {
'@odata.etag'?: string
}
interface PlannerTaskDetails {
export interface PlannerPlanDetails {
id: string
categoryDescriptions?: Record<string, string | null>
sharedWith?: Record<string, boolean>
'@odata.etag'?: string
}
export interface PlannerTaskDetails {
id: string
description?: string
previewType?: string
@@ -153,6 +165,28 @@ export interface MicrosoftPlannerReadPlanResponse extends ToolResponse {
}
}
export interface MicrosoftPlannerCreatePlanResponse extends ToolResponse {
output: {
plan: PlannerPlan
metadata: MicrosoftPlannerMetadata
}
}
export interface MicrosoftPlannerDeletePlanResponse extends ToolResponse {
output: {
deleted: boolean
metadata: MicrosoftPlannerMetadata
}
}
export interface MicrosoftPlannerGetPlanDetailsResponse extends ToolResponse {
output: {
planDetails: PlannerPlanDetails
etag: string
metadata: MicrosoftPlannerMetadata
}
}
export interface MicrosoftPlannerListBucketsResponse extends ToolResponse {
output: {
buckets: PlannerBucket[]
@@ -203,6 +237,20 @@ export interface MicrosoftPlannerUpdateTaskDetailsResponse extends ToolResponse
}
}
export interface MicrosoftPlannerUpdatePlanResponse extends ToolResponse {
output: {
plan: PlannerPlan
metadata: MicrosoftPlannerMetadata
}
}
export interface MicrosoftPlannerUpdatePlanDetailsResponse extends ToolResponse {
output: {
planDetails: PlannerPlanDetails
metadata: MicrosoftPlannerMetadata
}
}
export interface MicrosoftPlannerToolParams {
accessToken: string
planId?: string
@@ -221,6 +269,9 @@ export interface MicrosoftPlannerToolParams {
checklist?: Record<string, any>
references?: Record<string, any>
previewType?: string
appliedCategories?: string
categoryDescriptions?: Record<string, any>
sharedWith?: Record<string, any>
}
export type MicrosoftPlannerResponse =
@@ -230,6 +281,9 @@ export type MicrosoftPlannerResponse =
| MicrosoftPlannerDeleteTaskResponse
| MicrosoftPlannerListPlansResponse
| MicrosoftPlannerReadPlanResponse
| MicrosoftPlannerCreatePlanResponse
| MicrosoftPlannerDeletePlanResponse
| MicrosoftPlannerGetPlanDetailsResponse
| MicrosoftPlannerListBucketsResponse
| MicrosoftPlannerReadBucketResponse
| MicrosoftPlannerCreateBucketResponse
@@ -237,3 +291,5 @@ export type MicrosoftPlannerResponse =
| MicrosoftPlannerDeleteBucketResponse
| MicrosoftPlannerGetTaskDetailsResponse
| MicrosoftPlannerUpdateTaskDetailsResponse
| MicrosoftPlannerUpdatePlanResponse
| MicrosoftPlannerUpdatePlanDetailsResponse
@@ -1,7 +1,9 @@
import { createLogger } from '@sim/logger'
import { ErrorExtractorId } from '@/tools/error-extractors'
import type {
MicrosoftPlannerToolParams,
MicrosoftPlannerUpdateBucketResponse,
PlannerBucket,
} from '@/tools/microsoft_planner/types'
import type { ToolConfig } from '@/tools/types'
@@ -15,7 +17,7 @@ export const updateBucketTool: ToolConfig<
name: 'Update Microsoft Planner Bucket',
description: 'Update a bucket in Microsoft Planner',
version: '1.0',
errorExtractor: 'nested-error-object',
errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS,
oauth: {
required: true,
@@ -51,10 +53,11 @@ export const updateBucketTool: ToolConfig<
request: {
url: (params) => {
if (!params.bucketId) {
const bucketId = params.bucketId?.trim()
if (!bucketId) {
throw new Error('Bucket ID is required')
}
return `https://graph.microsoft.com/v1.0/planner/buckets/${params.bucketId}`
return `https://graph.microsoft.com/v1.0/planner/buckets/${bucketId}`
},
method: 'PATCH',
headers: (params) => {
@@ -80,6 +83,7 @@ export const updateBucketTool: ToolConfig<
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
Prefer: 'return=representation',
'If-Match': cleanedEtag,
}
},
@@ -99,8 +103,24 @@ export const updateBucketTool: ToolConfig<
},
},
transformResponse: async (response: Response) => {
const bucket = await response.json()
transformResponse: async (response: Response, params?: MicrosoftPlannerToolParams) => {
// Prefer: return=representation requests a body, but the service may still return
// 204 No Content for some tenants/requests
const text = await response.text()
if (!text || text.trim() === '') {
logger.info('Update successful but no response body returned (204 No Content)')
return {
success: true,
output: {
bucket: {} as PlannerBucket,
metadata: {
bucketId: params?.bucketId?.trim(),
},
},
}
}
const bucket = JSON.parse(text)
logger.info('Updated bucket:', bucket)
const result: MicrosoftPlannerUpdateBucketResponse = {
@@ -0,0 +1,144 @@
import { createLogger } from '@sim/logger'
import { ErrorExtractorId } from '@/tools/error-extractors'
import type {
MicrosoftPlannerToolParams,
MicrosoftPlannerUpdatePlanResponse,
PlannerPlan,
} from '@/tools/microsoft_planner/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('MicrosoftPlannerUpdatePlan')
export const updatePlanTool: ToolConfig<
MicrosoftPlannerToolParams,
MicrosoftPlannerUpdatePlanResponse
> = {
id: 'microsoft_planner_update_plan',
name: 'Update Microsoft Planner Plan',
description: 'Rename a Microsoft Planner plan',
version: '1.0',
errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS,
oauth: {
required: true,
provider: 'microsoft-planner',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Planner API',
},
planId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the plan to update (e.g., "xqQg5FS2LkCe54tAMV_v2ZgADW2J")',
},
etag: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The ETag value from the plan to update (If-Match header)',
},
title: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The new title of the plan',
},
},
request: {
url: (params) => {
const planId = params.planId?.trim()
if (!planId) {
throw new Error('Plan ID is required')
}
return `https://graph.microsoft.com/v1.0/planner/plans/${planId}`
},
method: 'PATCH',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
if (!params.etag) {
throw new Error('ETag is required for update operations')
}
let cleanedEtag = params.etag.trim()
while (cleanedEtag.startsWith('"') && cleanedEtag.endsWith('"')) {
cleanedEtag = cleanedEtag.slice(1, -1)
}
if (cleanedEtag.includes('\\"')) {
cleanedEtag = cleanedEtag.replace(/\\"/g, '"')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
Prefer: 'return=representation',
'If-Match': cleanedEtag,
}
},
body: (params) => {
if (!params.title?.trim()) {
throw new Error('Plan title is required')
}
const body = { title: params.title.trim() }
logger.info('Updating plan with body:', body)
return body
},
},
transformResponse: async (response: Response, params?: MicrosoftPlannerToolParams) => {
// Prefer: return=representation requests a body, but the service may still return
// 204 No Content for some tenants/requests
const text = await response.text()
if (!text || text.trim() === '') {
logger.info('Update successful but no response body returned (204 No Content)')
return {
success: true,
output: {
plan: {} as PlannerPlan,
metadata: {
planId: params?.planId?.trim(),
},
},
}
}
const plan = JSON.parse(text)
logger.info('Updated plan:', plan)
const result: MicrosoftPlannerUpdatePlanResponse = {
success: true,
output: {
plan,
metadata: {
planId: plan.id,
},
},
}
return result
},
outputs: {
success: { type: 'boolean', description: 'Whether the plan was updated successfully' },
plan: { type: 'object', description: 'The updated plan object with all properties' },
metadata: {
type: 'object',
description: 'Metadata including planId',
properties: {
planId: { type: 'string', description: 'Updated plan ID' },
},
},
},
}
@@ -0,0 +1,181 @@
import { createLogger } from '@sim/logger'
import { ErrorExtractorId } from '@/tools/error-extractors'
import type {
MicrosoftPlannerToolParams,
MicrosoftPlannerUpdatePlanDetailsResponse,
PlannerPlanDetails,
} from '@/tools/microsoft_planner/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('MicrosoftPlannerUpdatePlanDetails')
export const updatePlanDetailsTool: ToolConfig<
MicrosoftPlannerToolParams,
MicrosoftPlannerUpdatePlanDetailsResponse
> = {
id: 'microsoft_planner_update_plan_details',
name: 'Update Microsoft Planner Plan Details',
description:
"Update a plan's category (color label) descriptions and shared-with user list in Microsoft Planner",
version: '1.0',
errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS,
oauth: {
required: true,
provider: 'microsoft-planner',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Planner API',
},
planId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the plan (e.g., "xqQg5FS2LkCe54tAMV_v2ZgADW2J")',
},
etag: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The ETag value from the plan details to update (If-Match header)',
},
categoryDescriptions: {
type: 'object',
required: false,
visibility: 'user-only',
description:
'Category label names as a JSON object, e.g. {"category1": "Blocked", "category2": "At Risk"}. Set a value to null to clear a label.',
},
sharedWith: {
type: 'object',
required: false,
visibility: 'user-only',
description:
'User IDs to share the plan with as a JSON object, e.g. {"<user-id>": true}. Set a value to false to unshare.',
},
},
request: {
url: (params) => {
const planId = params.planId?.trim()
if (!planId) {
throw new Error('Plan ID is required')
}
return `https://graph.microsoft.com/v1.0/planner/plans/${planId}/details`
},
method: 'PATCH',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
if (!params.etag) {
throw new Error('ETag is required for update operations')
}
let cleanedEtag = params.etag.trim()
while (cleanedEtag.startsWith('"') && cleanedEtag.endsWith('"')) {
cleanedEtag = cleanedEtag.slice(1, -1)
}
if (cleanedEtag.includes('\\"')) {
cleanedEtag = cleanedEtag.replace(/\\"/g, '"')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
Prefer: 'return=representation',
'If-Match': cleanedEtag,
}
},
body: (params) => {
const body: Record<string, any> = {}
if (params.categoryDescriptions) {
try {
body.categoryDescriptions =
typeof params.categoryDescriptions === 'string'
? JSON.parse(params.categoryDescriptions)
: params.categoryDescriptions
} catch {
throw new Error('categoryDescriptions must be valid JSON')
}
}
if (params.sharedWith) {
try {
body.sharedWith =
typeof params.sharedWith === 'string'
? JSON.parse(params.sharedWith)
: params.sharedWith
} catch {
throw new Error('sharedWith must be valid JSON')
}
}
if (Object.keys(body).length === 0) {
throw new Error('At least one field must be provided to update')
}
logger.info('Updating plan details with body:', body)
return body
},
},
transformResponse: async (response: Response, params?: MicrosoftPlannerToolParams) => {
// Prefer: return=representation requests a body, but the service may still return
// 204 No Content for some tenants/requests
const text = await response.text()
if (!text || text.trim() === '') {
logger.info('Update successful but no response body returned (204 No Content)')
return {
success: true,
output: {
planDetails: {} as PlannerPlanDetails,
metadata: {
planId: params?.planId?.trim(),
},
},
}
}
const planDetails = JSON.parse(text)
logger.info('Updated plan details:', planDetails)
const result: MicrosoftPlannerUpdatePlanDetailsResponse = {
success: true,
output: {
planDetails,
metadata: {
planId: planDetails.id,
},
},
}
return result
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the plan details were updated successfully',
},
planDetails: {
type: 'object',
description: 'The updated plan details object with categoryDescriptions and sharedWith',
},
metadata: {
type: 'object',
description: 'Metadata including planId',
properties: {
planId: { type: 'string', description: 'Plan ID' },
},
},
},
}
+40 -10
View File
@@ -1,4 +1,5 @@
import { createLogger } from '@sim/logger'
import { ErrorExtractorId } from '@/tools/error-extractors'
import type {
MicrosoftPlannerToolParams,
MicrosoftPlannerUpdateTaskResponse,
@@ -16,7 +17,7 @@ export const updateTaskTool: ToolConfig<
name: 'Update Microsoft Planner Task',
description: 'Update a task in Microsoft Planner',
version: '1.0',
errorExtractor: 'nested-error-object',
errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS,
oauth: {
required: true,
@@ -86,14 +87,22 @@ export const updateTaskTool: ToolConfig<
description:
'The user ID to assign the task to (e.g., "e82f74c3-4d8a-4b5c-9f1e-2a6b8c9d0e3f")',
},
appliedCategories: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated category labels to apply to the task, e.g. "category1,category3" (up to category1-category25, plan-defined color labels)',
},
},
request: {
url: (params) => {
if (!params.taskId) {
const taskId = params.taskId?.trim()
if (!taskId) {
throw new Error('Task ID is required')
}
return `https://graph.microsoft.com/v1.0/planner/tasks/${params.taskId}`
return `https://graph.microsoft.com/v1.0/planner/tasks/${taskId}`
},
method: 'PATCH',
headers: (params) => {
@@ -123,6 +132,7 @@ export const updateTaskTool: ToolConfig<
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
Prefer: 'return=representation',
'If-Match': cleanedEtag,
}
},
@@ -157,7 +167,7 @@ export const updateTaskTool: ToolConfig<
body.percentComplete = params.percentComplete
}
if (params.priority !== undefined) {
if (params.priority !== undefined && params.priority !== null) {
body.priority = Number(params.priority)
}
@@ -168,12 +178,27 @@ export const updateTaskTool: ToolConfig<
) {
body.assignments = {
[params.assigneeUserId]: {
'@odata.type': 'microsoft.graph.plannerAssignment',
'@odata.type': '#microsoft.graph.plannerAssignment',
orderHint: ' !',
},
}
}
if (params.appliedCategories?.trim()) {
const categories = params.appliedCategories
.split(',')
.map((category) => category.trim())
.filter(Boolean)
if (categories.length > 0) {
body.appliedCategories = Object.fromEntries(
categories.map((category) =>
category.startsWith('-') ? [category.slice(1), false] : [category, true]
)
)
}
}
if (Object.keys(body).length === 0) {
throw new Error('At least one field must be provided to update')
}
@@ -184,19 +209,24 @@ export const updateTaskTool: ToolConfig<
},
transformResponse: async (response: Response, params?: MicrosoftPlannerToolParams) => {
// Check if response has content before parsing
// Check if response has content before parsing (Prefer: return=representation requests a
// body, but the service may still return 204 No Content for some tenants/requests)
const text = await response.text()
if (!text || text.trim() === '') {
logger.info('Update successful but no response body returned (204 No Content)')
return {
success: true,
output: {
message: 'Task updated successfully',
// Graph returned no body, so the etag sent in this request is now stale (the
// update changed it) and the actual new value is unknown. Returning it here would
// let a chained update silently reuse a stale If-Match and fail with 412 — leave
// it empty so callers re-fetch the task before their next update.
message: 'Task updated successfully (re-fetch the task to get its current etag)',
task: {} as PlannerTask,
taskId: params?.taskId || '',
etag: params?.etag || '',
taskId: params?.taskId?.trim() || '',
etag: '',
metadata: {
taskId: params?.taskId,
taskId: params?.taskId?.trim(),
},
},
}
@@ -1,7 +1,9 @@
import { createLogger } from '@sim/logger'
import { ErrorExtractorId } from '@/tools/error-extractors'
import type {
MicrosoftPlannerToolParams,
MicrosoftPlannerUpdateTaskDetailsResponse,
PlannerTaskDetails,
} from '@/tools/microsoft_planner/types'
import type { ToolConfig } from '@/tools/types'
@@ -16,7 +18,7 @@ export const updateTaskDetailsTool: ToolConfig<
description:
'Update task details including description, checklist items, and references in Microsoft Planner',
version: '1.0',
errorExtractor: 'nested-error-object',
errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS,
oauth: {
required: true,
@@ -70,10 +72,11 @@ export const updateTaskDetailsTool: ToolConfig<
request: {
url: (params) => {
if (!params.taskId) {
const taskId = params.taskId?.trim()
if (!taskId) {
throw new Error('Task ID is required')
}
return `https://graph.microsoft.com/v1.0/planner/tasks/${params.taskId}/details`
return `https://graph.microsoft.com/v1.0/planner/tasks/${taskId}/details`
},
method: 'PATCH',
headers: (params) => {
@@ -127,11 +130,23 @@ export const updateTaskDetailsTool: ToolConfig<
}
if (params.checklist) {
body.checklist = params.checklist
try {
body.checklist =
typeof params.checklist === 'string' ? JSON.parse(params.checklist) : params.checklist
} catch {
throw new Error('Checklist must be valid JSON')
}
}
if (params.references) {
body.references = params.references
try {
body.references =
typeof params.references === 'string'
? JSON.parse(params.references)
: params.references
} catch {
throw new Error('References must be valid JSON')
}
}
if (params.previewType) {
@@ -147,8 +162,24 @@ export const updateTaskDetailsTool: ToolConfig<
},
},
transformResponse: async (response: Response) => {
const taskDetails = await response.json()
transformResponse: async (response: Response, params?: MicrosoftPlannerToolParams) => {
// Prefer: return=representation requests a body, but the service may still return
// 204 No Content for some tenants/requests
const text = await response.text()
if (!text || text.trim() === '') {
logger.info('Update successful but no response body returned (204 No Content)')
return {
success: true,
output: {
taskDetails: {} as PlannerTaskDetails,
metadata: {
taskId: params?.taskId?.trim(),
},
},
}
}
const taskDetails = JSON.parse(text)
logger.info('Updated task details:', taskDetails)
const result: MicrosoftPlannerUpdateTaskDetailsResponse = {
+10
View File
@@ -2334,9 +2334,12 @@ import {
} from '@/tools/microsoft_excel'
import {
microsoftPlannerCreateBucketTool,
microsoftPlannerCreatePlanTool,
microsoftPlannerCreateTaskTool,
microsoftPlannerDeleteBucketTool,
microsoftPlannerDeletePlanTool,
microsoftPlannerDeleteTaskTool,
microsoftPlannerGetPlanDetailsTool,
microsoftPlannerGetTaskDetailsTool,
microsoftPlannerListBucketsTool,
microsoftPlannerListPlansTool,
@@ -2344,6 +2347,8 @@ import {
microsoftPlannerReadPlanTool,
microsoftPlannerReadTaskTool,
microsoftPlannerUpdateBucketTool,
microsoftPlannerUpdatePlanDetailsTool,
microsoftPlannerUpdatePlanTool,
microsoftPlannerUpdateTaskDetailsTool,
microsoftPlannerUpdateTaskTool,
} from '@/tools/microsoft_planner'
@@ -7623,6 +7628,11 @@ export const tools: Record<string, ToolConfig> = {
microsoft_planner_delete_task: microsoftPlannerDeleteTaskTool,
microsoft_planner_list_plans: microsoftPlannerListPlansTool,
microsoft_planner_read_plan: microsoftPlannerReadPlanTool,
microsoft_planner_create_plan: microsoftPlannerCreatePlanTool,
microsoft_planner_update_plan: microsoftPlannerUpdatePlanTool,
microsoft_planner_get_plan_details: microsoftPlannerGetPlanDetailsTool,
microsoft_planner_update_plan_details: microsoftPlannerUpdatePlanDetailsTool,
microsoft_planner_delete_plan: microsoftPlannerDeletePlanTool,
microsoft_planner_list_buckets: microsoftPlannerListBucketsTool,
microsoft_planner_read_bucket: microsoftPlannerReadBucketTool,
microsoft_planner_create_bucket: microsoftPlannerCreateBucketTool,