Skip to content

Commit 6ff6255

Browse files
feat(outlook): add Microsoft Graph calendar operations (#6041)
* feat(outlook): add Calendars.ReadWrite and MailboxSettings.Read scopes Extend the shared outlook OAuth service with delegated Graph calendar scopes so one Microsoft connection covers mail and calendar. Existing connected users must reconnect to be granted the new scopes (noted in a code comment). Adds human-readable scope descriptions and test assertions. * feat(outlook): add Microsoft Graph calendar tools Six calendar operations against graph.microsoft.com/v1.0: list events (calendarView with nextLink paging), get, create, update (partial PATCH), delete, and respond to invites. Shared calendar-utils handles Graph's offset-less dateTime+timeZone shape, attendee normalization, and event flattening. All tools carry the Microsoft Graph error extractor and 429/backoff retry (honors Retry-After) for the mailbox concurrency limit. Registered in the tool registry and barrel. * feat(outlook): surface calendar operations in the Outlook block Add six calendar operations to the Outlook block operation dropdown with their conditional subBlocks, tool wiring, inputs, and event-shaped outputs. Mail operations are unchanged and backward compatible. * fix(outlook): address calendar review findings - Validate list-events pageToken origin with assertGraphNextPageUrl (matches the onedrive/microsoft_ad Graph paging guard) so a workflow-supplied URL can't receive the Outlook bearer token. - All-day create/update now normalize both bounds to midnight and force an exclusive end day (buildAllDayRange), instead of sending a zero-length same-midnight window that Graph rejects. - Drop the stale 'suggest meeting times' claim from the block longDescription. - Remove the unused MailboxSettings.Read scope (least privilege; no tool reads it). * feat(outlook): add calendar picker and harden calendar tools Validation pass over the new Microsoft Graph calendar operations against the v1.0 API docs, plus the calendar selection the tools were missing. - Add an `outlook.calendars` picker (GET /me/calendars) with basic selector + advanced manual ID, wired through the `calendarId` canonical param. List and create now target `/me/calendars/{id}/...`; get/update/delete/respond keep using `/me/events/{id}` since event IDs are mailbox-unique. - Fix all-day update rejecting when only one bound is supplied — both bounds are now normalized to midnight with an exclusive end day. - Fix "Send Response to Organizer" reading as OFF while Graph's default is to notify; it is now a dropdown defaulting to Yes, and the param is always sent. - Add timestamp wandConfig to the four calendar datetime fields and a list wandConfig to attendees. - Centralize Graph URL construction in calendar-utils; guard maxResults against non-numeric input and trim the calendarView time-window bounds. - Add three calendar templates and four calendar skills to OutlookBlockMeta. - Regenerate integration docs. * fix(outlook): scope online-meeting comments to what Graph documents onlineMeetingProvider is optional and defaults to unknown; the docs state that setting isOnlineMeeting alone initializes onlineMeeting. They do not document Graph substituting the calendar's defaultOnlineMeetingProvider, so the comments now claim only that, plus the real reason not to pin teamsForBusiness (mailboxes that disallow it via allowedOnlineMeetingProviders). * fix(outlook): allow calendar paging without re-supplying the time window Cursor Bugbot round: startDateTime/endDateTime were required tool params, so a paging call carrying only pageToken failed validateToolParameters before the request was built — even though the url builder short-circuits on pageToken and ignores both bounds. Relax them to optional and enforce the real invariant (pageToken OR both bounds) in the url builder, matching tools/sharepoint/list_sites.ts. Block subblocks stay required, so the normal editor flow is unchanged. Also correct the calendar_respond comment: Graph documents exactly two 400 conditions for accept/decline, both on proposedNewTime, which we never send. A non-empty comment alongside sendResponse=false is valid. * feat(outlook): add Calendars.ReadWrite.Shared for shared calendars The calendar picker lists /me/calendars, which can include calendars other users have shared with or delegated to the account. Calendars.ReadWrite covers only the user's own calendars, so selecting a shared team calendar would 403 on both read and write. Calendars.ReadWrite is kept alongside it, not replaced: Graph documents it as the sole accepted permission for creating and updating events and for accept/tentativelyAccept/decline (all list "Higher: Not available"), so the .Shared scope does not subsume it. Added now rather than later because this PR already forces existing Outlook users to re-consent for Calendars.ReadWrite; deferring would cost them a second reconnect. * fix(outlook): treat date-only bounds as all-day and guard partial all-day updates Cursor round 2: - Date-only bounds no longer produce a zero-length window. The param docs invited a date like 2025-06-03 for an all-day event, but buildAllDayRange only ran when isAllDay was explicitly true, so date-only input built a 00:00->00:00 timed window that Graph rejects. A date-only bound carries no time, so the only coherent reading is all-day; create/update now promote on that shape and the descriptions state it. - Converting an event to all-day with no bounds now fails with an actionable message instead of a Graph 400. Graph requires all-day events to have midnight start and end in the same zone, and those cannot be derived from a partial PATCH against an event whose existing bounds are timed. * docs(outlook): note that the calendar window fields are ignored when paging * fix(outlook): don't promote a lone date-only bound to all-day on update Regression from the previous round's date-only promotion. A single date-only startDateTime or endDateTime satisfied "all provided bounds are date-only", so the tool promoted to all-day and derived the missing side from the supplied one — turning a partial reschedule of a timed or multi-day event into a one-day all-day event and dropping the original other bound. Implicit promotion now requires BOTH bounds to be date-only, matching calendar_create_event. A lone date-only bound is ambiguous (convert to all-day, or just move that edge?) and a PATCH cannot read the event's existing bounds to disambiguate, so it stays on the timed path and leaves the other side untouched. Deriving a missing bound remains allowed when isAllDay is set explicitly, since that is stated intent rather than a guess. Also pins the explicit-isAllDay-false + date-only override with a regression test: the block always sends isAllDay for create, so an untouched switch arrives as false, and the data shape has to win or the fix would be unreachable from the UI. * revert(outlook): drop Calendars.ReadWrite.Shared from the outlook provider Reverts the scope I added two rounds ago. It was the wrong call. This provider is shared by work/school AND personal Outlook accounts, and the .Shared calendar scopes are not confirmed supported for personal Microsoft accounts. Requesting one risks failing consent for personal users — which would take mail access down with it, breaking functionality that works today. The PR already documents this exact reasoning as why findMeetingTimes was excluded, and that decision was made against a live personal mailbox. The evidence I added it on was a summarized read of the permissions reference claiming MSA support; a targeted follow-up could not confirm it for Calendars.ReadWrite.Shared specifically. Given the asymmetry — broken consent for all personal users vs. a shared-calendar feature gap — least privilege wins. Calendar operations therefore target calendars the account owns. The calendarId param descriptions now say a calendar shared by another user may return 403, and the scope list carries a comment explaining why .Shared must not be re-added. * fix(outlook): make retried event creates duplicate-safe via transactionId The retry config opts POSTs in via retryIdempotentOnly:false, but the executor's isRetryableFailure covers 429 AND 500-599 — not just the throttle the comment justified. A 5xx returned after Graph had already committed a create would be retried and produce a duplicate calendar event. create_event now sends a transactionId, which Graph documents for exactly this: it discards a repeat POST carrying an id it has already seen. The request body is built once per execution (formatRequestParams runs before the attempt loop), so the id is stable across retries of a call and unique between calls. The retry comment now describes what actually retries and why each non-idempotent method is safe: PATCH replays the same partial body as a no-op, and respond is state-idempotent though a post-commit retry can send the organizer a second notification — accepted deliberately, since Graph exposes no transactionId for accept/decline and failing outright under throttling is worse. * fix(outlook): tighten date-only detection and align online-meeting copy Final validation pass over the calendar integration. - isDateOnly matched "contains no T", so a space-separated datetime (2025-06-03 10:00) counted as date-only: the time was discarded and the value built as '2025-06-03 10:00T00:00:00', which Graph rejects. It now matches YYYY-MM-DD strictly, and buildGraphEventDateTime normalizes the space form to ISO rather than mangling it, so a natural input works instead of 400ing. - The isOnlineMeeting param descriptions still claimed Graph 'uses the mailbox default provider' — the same unverified mechanism already removed from the code comments. They now state only what the docs and the author's live testing support: the join URL depends on the providers the mailbox allows, and stays null on personal accounts. - Adds blocks/blocks/outlook.test.ts following the repo's per-block test convention: every calendar operation resolves to a registered tool in tools.access, supplies all required tool params, emits no params the tool cannot accept, maps one-to-one onto the calendar tools, and the calendarId canonical group and sendResponse default are pinned. --------- Co-authored-by: Waleed Latif <walif6@gmail.com>
1 parent e369997 commit 6ff6255

25 files changed

Lines changed: 3124 additions & 12 deletions

File tree

apps/docs/content/docs/en/integrations/outlook.mdx

Lines changed: 228 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Outlook
3-
description: Send, read, search, reply, organize, and manage Outlook email
3+
description: Send, read, search, reply, organize, and manage Outlook email and calendar
44
---
55

66
import { BlockInfoCard } from "@/components/ui/block-info-card"
@@ -37,7 +37,7 @@ By connecting Sim with Microsoft Outlook, you enable intelligent agents to autom
3737

3838
## Usage Instructions
3939

40-
Integrate Outlook into the workflow. Can send, draft, read, search, reply, forward, move, copy, and delete email; manage mail folders and attachments; and set categories and flags on messages. Can be used in trigger mode to trigger a workflow when a new email is received.
40+
Integrate Outlook into the workflow. Can send, draft, read, search, reply, forward, move, copy, and delete email; manage mail folders and attachments; and set categories and flags on messages. Can also list, create, update, delete, and respond to calendar events. Can be used in trigger mode to trigger a workflow when a new email is received.
4141

4242

4343

@@ -473,6 +473,232 @@ Get a single attachment on an Outlook message, including its file contents
473473
|`lastModifiedDateTime` | string | When the attachment was last modified \(ISO 8601\) |
474474
| `attachments` | file[] | The downloaded file attachment \(empty for non-file attachment types\) |
475475

476+
### `outlook_calendar_list_events`
477+
478+
List Outlook calendar events within a start/end time window
479+
480+
#### Input
481+
482+
| Parameter | Type | Required | Description |
483+
| --------- | ---- | -------- | ----------- |
484+
| `calendarId` | string | No | ID of the calendar to read. Defaults to the mailbox default calendar. Calendars shared by another user are not supported and may return 403. |
485+
| `startDateTime` | string | No | Start of the time window \(ISO 8601, e.g. 2025-06-03T00:00:00-08:00\). Interpreted as UTC if no offset is given. Required unless paging with pageToken. |
486+
| `endDateTime` | string | No | End of the time window \(ISO 8601, e.g. 2025-06-10T00:00:00-08:00\). Interpreted as UTC if no offset is given. Required unless paging with pageToken. |
487+
| `maxResults` | number | No | Maximum number of events to return per page \(default: 10, max: 100\) |
488+
| `orderBy` | string | No | Order of events \(default: start/dateTime\) |
489+
| `pageToken` | string | No | Full @odata.nextLink URL from a previous page to continue paging |
490+
491+
#### Output
492+
493+
| Parameter | Type | Description |
494+
| --------- | ---- | ----------- |
495+
| `message` | string | Success or status message |
496+
| `results` | array | Array of calendar event objects |
497+
|`id` | string | Unique event identifier |
498+
|`subject` | string | Event subject/title |
499+
|`bodyPreview` | string | Preview of the event body |
500+
|`start` | object | Event start |
501+
|`dateTime` | string | Local date and time \(ISO 8601, no offset\) |
502+
|`timeZone` | string | IANA or Windows time zone name |
503+
|`end` | object | Event end |
504+
|`dateTime` | string | Local date and time \(ISO 8601, no offset\) |
505+
|`timeZone` | string | IANA or Windows time zone name |
506+
|`isAllDay` | boolean | Whether the event lasts the entire day |
507+
|`location` | string | Event location display name |
508+
|`organizer` | object | Event organizer |
509+
|`name` | string | Display name of the person or entity |
510+
|`address` | string | Email address |
511+
|`attendees` | array | Event attendees |
512+
|`name` | string | Attendee display name |
513+
|`address` | string | Attendee email address |
514+
|`type` | string | Attendee type \(required, optional, or resource\) |
515+
|`response` | string | Attendee response status \(none, accepted, declined, tentativelyAccepted, ...\) |
516+
|`onlineMeeting` | object | Online-meeting join details, if any |
517+
|`joinUrl` | string | URL to join the online meeting |
518+
|`webLink` | string | URL that opens the event in Outlook on the web |
519+
| `nextLink` | string | URL for the next page of results, if any |
520+
521+
### `outlook_calendar_get_event`
522+
523+
Get a single Outlook calendar event by ID
524+
525+
#### Input
526+
527+
| Parameter | Type | Required | Description |
528+
| --------- | ---- | -------- | ----------- |
529+
| `eventId` | string | Yes | The ID of the calendar event to retrieve |
530+
531+
#### Output
532+
533+
| Parameter | Type | Description |
534+
| --------- | ---- | ----------- |
535+
| `message` | string | Success or status message |
536+
| `results` | object | The calendar event object |
537+
|`id` | string | Unique event identifier |
538+
|`subject` | string | Event subject/title |
539+
|`bodyPreview` | string | Preview of the event body |
540+
|`start` | object | Event start |
541+
|`dateTime` | string | Local date and time \(ISO 8601, no offset\) |
542+
|`timeZone` | string | IANA or Windows time zone name |
543+
|`end` | object | Event end |
544+
|`dateTime` | string | Local date and time \(ISO 8601, no offset\) |
545+
|`timeZone` | string | IANA or Windows time zone name |
546+
|`isAllDay` | boolean | Whether the event lasts the entire day |
547+
|`location` | string | Event location display name |
548+
|`organizer` | object | Event organizer |
549+
|`name` | string | Display name of the person or entity |
550+
|`address` | string | Email address |
551+
|`attendees` | array | Event attendees |
552+
|`name` | string | Attendee display name |
553+
|`address` | string | Attendee email address |
554+
|`type` | string | Attendee type \(required, optional, or resource\) |
555+
|`response` | string | Attendee response status \(none, accepted, declined, tentativelyAccepted, ...\) |
556+
|`onlineMeeting` | object | Online-meeting join details, if any |
557+
|`joinUrl` | string | URL to join the online meeting |
558+
|`webLink` | string | URL that opens the event in Outlook on the web |
559+
560+
### `outlook_calendar_create_event`
561+
562+
Create a new Outlook calendar event
563+
564+
#### Input
565+
566+
| Parameter | Type | Required | Description |
567+
| --------- | ---- | -------- | ----------- |
568+
| `calendarId` | string | No | ID of the calendar to create the event in. Defaults to the mailbox default calendar. Calendars shared by another user are not supported and may return 403. |
569+
| `subject` | string | Yes | Event subject/title |
570+
| `startDateTime` | string | Yes | Start time \(ISO 8601, e.g. 2025-06-03T10:00:00-08:00\). A date-only value \(2025-06-03\) for both start and end creates an all-day event. |
571+
| `endDateTime` | string | Yes | End time \(ISO 8601, e.g. 2025-06-03T11:00:00-08:00\). A date-only value \(2025-06-04\) for both start and end creates an all-day event. |
572+
| `timeZone` | string | No | IANA or Windows time zone name \(e.g. America/Los_Angeles\). Used for datetimes without a UTC offset. Defaults to UTC. |
573+
| `body` | string | No | Event body content |
574+
| `contentType` | string | No | Content type for the event body \(text or html\) |
575+
| `location` | string | No | Event location display name |
576+
| `attendees` | string | No | Attendee email addresses \(comma-separated\) |
577+
| `isAllDay` | boolean | No | Whether the event lasts the entire day |
578+
| `isOnlineMeeting` | boolean | No | Attach an online meeting to the event. The join URL depends on the meeting providers the mailbox allows \(Teams on work/school accounts\); personal accounts have no supported provider, so onlineMeeting.joinUrl stays null there. |
579+
580+
#### Output
581+
582+
| Parameter | Type | Description |
583+
| --------- | ---- | ----------- |
584+
| `message` | string | Success or status message |
585+
| `results` | object | The created calendar event object |
586+
|`id` | string | Unique event identifier |
587+
|`subject` | string | Event subject/title |
588+
|`bodyPreview` | string | Preview of the event body |
589+
|`start` | object | Event start |
590+
|`dateTime` | string | Local date and time \(ISO 8601, no offset\) |
591+
|`timeZone` | string | IANA or Windows time zone name |
592+
|`end` | object | Event end |
593+
|`dateTime` | string | Local date and time \(ISO 8601, no offset\) |
594+
|`timeZone` | string | IANA or Windows time zone name |
595+
|`isAllDay` | boolean | Whether the event lasts the entire day |
596+
|`location` | string | Event location display name |
597+
|`organizer` | object | Event organizer |
598+
|`name` | string | Display name of the person or entity |
599+
|`address` | string | Email address |
600+
|`attendees` | array | Event attendees |
601+
|`name` | string | Attendee display name |
602+
|`address` | string | Attendee email address |
603+
|`type` | string | Attendee type \(required, optional, or resource\) |
604+
|`response` | string | Attendee response status \(none, accepted, declined, tentativelyAccepted, ...\) |
605+
|`onlineMeeting` | object | Online-meeting join details, if any |
606+
|`joinUrl` | string | URL to join the online meeting |
607+
|`webLink` | string | URL that opens the event in Outlook on the web |
608+
609+
### `outlook_calendar_update_event`
610+
611+
Update an existing Outlook calendar event
612+
613+
#### Input
614+
615+
| Parameter | Type | Required | Description |
616+
| --------- | ---- | -------- | ----------- |
617+
| `eventId` | string | Yes | The ID of the calendar event to update |
618+
| `subject` | string | No | New event subject/title |
619+
| `startDateTime` | string | No | New start time \(ISO 8601\). A date-only value \(2025-06-03\) converts the event to all-day. |
620+
| `endDateTime` | string | No | New end time \(ISO 8601\). A date-only value \(2025-06-04\) converts the event to all-day. |
621+
| `timeZone` | string | No | IANA or Windows time zone name applied to updated datetimes without a UTC offset. Defaults to UTC. |
622+
| `body` | string | No | New event body content |
623+
| `contentType` | string | No | Content type for the event body \(text or html\) |
624+
| `location` | string | No | New event location display name |
625+
| `attendees` | string | No | Replacement attendee email addresses \(comma-separated\) |
626+
| `isAllDay` | boolean | No | Whether the event lasts the entire day. Setting this true requires also sending startDateTime, since Graph needs midnight bounds. |
627+
| `isOnlineMeeting` | boolean | No | Attach an online meeting to the event. The join URL depends on the meeting providers the mailbox allows \(Teams on work/school accounts\); personal accounts have no supported provider, so onlineMeeting.joinUrl stays null there. |
628+
629+
#### Output
630+
631+
| Parameter | Type | Description |
632+
| --------- | ---- | ----------- |
633+
| `message` | string | Success or status message |
634+
| `results` | object | The updated calendar event object |
635+
|`id` | string | Unique event identifier |
636+
|`subject` | string | Event subject/title |
637+
|`bodyPreview` | string | Preview of the event body |
638+
|`start` | object | Event start |
639+
|`dateTime` | string | Local date and time \(ISO 8601, no offset\) |
640+
|`timeZone` | string | IANA or Windows time zone name |
641+
|`end` | object | Event end |
642+
|`dateTime` | string | Local date and time \(ISO 8601, no offset\) |
643+
|`timeZone` | string | IANA or Windows time zone name |
644+
|`isAllDay` | boolean | Whether the event lasts the entire day |
645+
|`location` | string | Event location display name |
646+
|`organizer` | object | Event organizer |
647+
|`name` | string | Display name of the person or entity |
648+
|`address` | string | Email address |
649+
|`attendees` | array | Event attendees |
650+
|`name` | string | Attendee display name |
651+
|`address` | string | Attendee email address |
652+
|`type` | string | Attendee type \(required, optional, or resource\) |
653+
|`response` | string | Attendee response status \(none, accepted, declined, tentativelyAccepted, ...\) |
654+
|`onlineMeeting` | object | Online-meeting join details, if any |
655+
|`joinUrl` | string | URL to join the online meeting |
656+
|`webLink` | string | URL that opens the event in Outlook on the web |
657+
658+
### `outlook_calendar_delete_event`
659+
660+
Delete an Outlook calendar event by ID
661+
662+
#### Input
663+
664+
| Parameter | Type | Required | Description |
665+
| --------- | ---- | -------- | ----------- |
666+
| `eventId` | string | Yes | The ID of the calendar event to delete |
667+
668+
#### Output
669+
670+
| Parameter | Type | Description |
671+
| --------- | ---- | ----------- |
672+
| `message` | string | Success or status message |
673+
| `results` | object | Delete result details |
674+
|`eventId` | string | ID of the deleted event |
675+
|`status` | string | Deletion status |
676+
677+
### `outlook_calendar_respond`
678+
679+
Accept, tentatively accept, or decline an Outlook calendar event invitation
680+
681+
#### Input
682+
683+
| Parameter | Type | Required | Description |
684+
| --------- | ---- | -------- | ----------- |
685+
| `eventId` | string | Yes | The ID of the calendar event to respond to |
686+
| `responseType` | string | Yes | The response: accept, tentativelyAccept, or decline |
687+
| `comment` | string | No | Optional comment to include with the response |
688+
| `sendResponse` | boolean | No | Whether to send a response to the organizer \(default: true\) |
689+
690+
#### Output
691+
692+
| Parameter | Type | Description |
693+
| --------- | ---- | ----------- |
694+
| `message` | string | Success or status message |
695+
| `results` | object | Response result details |
696+
|`eventId` | string | ID of the event responded to |
697+
|`responseType` | string | The response that was sent |
698+
|`status` | string | Response status |
699+
|`httpStatus` | number | HTTP status code returned by the API |
700+
|`requestId` | string | Microsoft Graph request-id header for tracing |
701+
476702

477703

478704
## Triggers

0 commit comments

Comments
 (0)