Skip to content

Commit 0ff5e2f

Browse files
committed
feat: create calendar events on Google and Microsoft accounts
Adds the ability to create calendar events on connected Google and Microsoft accounts, mirroring the existing email-send architecture. Exposed three ways: - GraphQL mutation createCalendarEvent (metadata API) - AI agent tool create_calendar_event, gated by a new CREATE_CALENDAR_EVENT_TOOL permission flag - "Create Calendar Event" workflow builder node (Core section) The created event is persisted immediately by reusing the calendar import saver and reconciled by the next provider sync. Google needs no new scope; Microsoft moves from Calendars.Read to Calendars.ReadWrite.
1 parent fea2b87 commit 0ff5e2f

74 files changed

Lines changed: 2649 additions & 4 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/twenty-front/src/generated-metadata/graphql.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4113,6 +4113,7 @@ export enum PermissionFlagType {
41134113
BILLING = 'BILLING',
41144114
CODE_INTERPRETER_TOOL = 'CODE_INTERPRETER_TOOL',
41154115
CONNECTED_ACCOUNTS = 'CONNECTED_ACCOUNTS',
4116+
CREATE_CALENDAR_EVENT_TOOL = 'CREATE_CALENDAR_EVENT_TOOL',
41164117
DATA_MODEL = 'DATA_MODEL',
41174118
DOWNLOAD_FILE = 'DOWNLOAD_FILE',
41184119
EXPORT_CSV = 'EXPORT_CSV',

packages/twenty-front/src/modules/settings/roles/role-permissions/permission-flags/hooks/useActionRolePermissionFlagConfig.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useMemo } from 'react';
44
import {
55
IconApi,
66
IconAt,
7+
IconCalendarEvent,
78
IconCode,
89
IconDownload,
910
IconFileExport,
@@ -77,6 +78,16 @@ export const useActionRolePermissionFlagConfig = ({
7778
isRelevantForApiKeys: true,
7879
isRelevantForUsers: true,
7980
},
81+
{
82+
key: PermissionFlagType.CREATE_CALENDAR_EVENT_TOOL,
83+
name: t`Create Calendar Event`,
84+
description: t`Create calendar events via connected accounts`,
85+
Icon: IconCalendarEvent,
86+
isToolPermission: true,
87+
isRelevantForAgents: true,
88+
isRelevantForApiKeys: true,
89+
isRelevantForUsers: true,
90+
},
8091
{
8192
key: PermissionFlagType.HTTP_REQUEST_TOOL,
8293
name: t`HTTP Request`,
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export type CalendarEventFormData = {
2+
connectedAccountId: string;
3+
title: string;
4+
description: string;
5+
location: string;
6+
startsAt: string;
7+
endsAt: string;
8+
isFullDay: boolean;
9+
timeZone: string;
10+
attendees: string;
11+
sendInvitations: boolean;
12+
addConferencing: boolean;
13+
};

packages/twenty-front/src/modules/workflow/types/Workflow.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
type SingleRecordAvailability,
55
type workflowAiAgentActionSchema,
66
type workflowCodeActionSchema,
7+
type workflowCreateCalendarEventActionSchema,
78
type workflowCreateRecordActionSchema,
89
type workflowCronTriggerSchema,
910
type workflowDatabaseEventTriggerSchema,
@@ -42,6 +43,9 @@ export type WorkflowSendEmailAction = z.infer<
4243
export type WorkflowDraftEmailAction = z.infer<
4344
typeof workflowDraftEmailActionSchema
4445
>;
46+
export type WorkflowCreateCalendarEventAction = z.infer<
47+
typeof workflowCreateCalendarEventActionSchema
48+
>;
4549
export type WorkflowCreateRecordAction = z.infer<
4650
typeof workflowCreateRecordActionSchema
4751
>;
@@ -78,6 +82,7 @@ export type WorkflowAction =
7882
| WorkflowLogicFunctionAction
7983
| WorkflowSendEmailAction
8084
| WorkflowDraftEmailAction
85+
| WorkflowCreateCalendarEventAction
8186
| WorkflowCreateRecordAction
8287
| WorkflowUpdateRecordAction
8388
| WorkflowDeleteRecordAction

packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeIcon.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ export const WorkflowDiagramStepNodeIcon = ({
3535
case 'CODE':
3636
case 'HTTP_REQUEST':
3737
case 'SEND_EMAIL':
38-
case 'DRAFT_EMAIL': {
38+
case 'DRAFT_EMAIL':
39+
case 'CREATE_CALENDAR_EVENT': {
3940
return (
4041
<Icon
4142
size={theme.icon.size.md}

packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepNodeDetail.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
77
import { WorkflowEditActionAiAgent } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowEditActionAiAgent';
88
import { WorkflowActionCode } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowActionCode';
9+
import { WorkflowEditActionCreateCalendarEvent } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateCalendarEvent';
910
import { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord';
1011
import { WorkflowEditActionDeleteRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord';
1112
import { WorkflowEditActionEmpty } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmpty';
@@ -140,6 +141,17 @@ export const WorkflowRunStepNodeDetail = ({
140141
/>
141142
);
142143
}
144+
case 'CREATE_CALENDAR_EVENT': {
145+
return (
146+
<WorkflowEditActionCreateCalendarEvent
147+
key={stepId}
148+
action={stepDefinition.definition}
149+
actionOptions={{
150+
readonly: true,
151+
}}
152+
/>
153+
);
154+
}
143155
case 'CREATE_RECORD': {
144156
return (
145157
<WorkflowEditActionCreateRecord

packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepDetail.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
66
import { WorkflowEditActionAiAgent } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowEditActionAiAgent';
77
import { WorkflowActionCode } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowActionCode';
8+
import { WorkflowEditActionCreateCalendarEvent } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateCalendarEvent';
89
import { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord';
910
import { WorkflowEditActionDeleteRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord';
1011
import { WorkflowEditActionEmpty } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmpty';
@@ -136,6 +137,15 @@ export const WorkflowStepDetail = ({
136137
/>
137138
);
138139
}
140+
case 'CREATE_CALENDAR_EVENT': {
141+
return (
142+
<WorkflowEditActionCreateCalendarEvent
143+
key={stepId}
144+
action={stepDefinition.definition}
145+
actionOptions={props}
146+
/>
147+
);
148+
}
139149
case 'CREATE_RECORD': {
140150
return (
141151
<WorkflowEditActionCreateRecord
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { FormBooleanFieldToggleInput } from '@/object-record/record-field/ui/form-types/components/FormBooleanFieldToggleInput';
2+
import { FormMultiTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormMultiTextFieldInput';
3+
import { FormSelectFieldInput } from '@/object-record/record-field/ui/form-types/components/FormSelectFieldInput';
4+
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
5+
import { useMyConnectedAccounts } from '@/settings/accounts/hooks/useMyConnectedAccounts';
6+
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
7+
import { type WorkflowCreateCalendarEventAction } from '@/workflow/types/Workflow';
8+
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
9+
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
10+
import { useCalendarEventForm } from '@/workflow/workflow-steps/workflow-actions/hooks/useCalendarEventForm';
11+
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
12+
import { t } from '@lingui/core/macro';
13+
import { useEffect } from 'react';
14+
import { ConnectedAccountProvider, SettingsPath } from 'twenty-shared/types';
15+
import { type SelectOption } from 'twenty-ui/input';
16+
import { IconPlus } from 'twenty-ui/icon';
17+
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
18+
19+
// Calendar event creation is only supported on Google and Microsoft accounts.
20+
const CALENDAR_CAPABLE_PROVIDERS = [
21+
ConnectedAccountProvider.GOOGLE,
22+
ConnectedAccountProvider.MICROSOFT,
23+
];
24+
25+
type WorkflowEditActionCreateCalendarEventProps = {
26+
action: WorkflowCreateCalendarEventAction;
27+
actionOptions:
28+
| {
29+
readonly: true;
30+
}
31+
| {
32+
readonly?: false;
33+
onActionUpdate: (action: WorkflowCreateCalendarEventAction) => void;
34+
};
35+
};
36+
37+
export const WorkflowEditActionCreateCalendarEvent = ({
38+
action,
39+
actionOptions,
40+
}: WorkflowEditActionCreateCalendarEventProps) => {
41+
const { formData, handleFieldChange, saveAction } = useCalendarEventForm({
42+
action,
43+
onActionUpdate:
44+
actionOptions.readonly === true
45+
? undefined
46+
: actionOptions.onActionUpdate,
47+
readonly: actionOptions.readonly === true,
48+
});
49+
50+
const navigate = useNavigateSettings();
51+
const { closeSidePanelMenu } = useSidePanelMenu();
52+
const { accounts: myAccounts, loading } = useMyConnectedAccounts();
53+
54+
const connectedAccountOptions: SelectOption<string>[] = myAccounts
55+
.filter((account) => CALENDAR_CAPABLE_PROVIDERS.includes(account.provider))
56+
.map((account) => ({ label: account.handle, value: account.id }));
57+
58+
useEffect(() => {
59+
return () => {
60+
saveAction.flush();
61+
};
62+
}, [saveAction]);
63+
64+
if (loading) {
65+
return null;
66+
}
67+
68+
return (
69+
<>
70+
<WorkflowStepBody>
71+
<FormSelectFieldInput
72+
key={`connected-account-${formData.connectedAccountId || 'none'}`}
73+
label={t`Account`}
74+
hint={t`Google or Microsoft account to create the event on. Leave empty to use the default calendar account.`}
75+
defaultValue={formData.connectedAccountId}
76+
options={connectedAccountOptions}
77+
onChange={(value) =>
78+
handleFieldChange('connectedAccountId', value ?? '')
79+
}
80+
readonly={actionOptions.readonly}
81+
callToActionButton={{
82+
onClick: () => {
83+
closeSidePanelMenu();
84+
navigate(SettingsPath.NewAccount);
85+
},
86+
Icon: IconPlus,
87+
text: t`Add account`,
88+
}}
89+
/>
90+
<FormTextFieldInput
91+
label={t`Title`}
92+
placeholder={t`Enter event title`}
93+
readonly={actionOptions.readonly}
94+
defaultValue={formData.title}
95+
onChange={(value) => handleFieldChange('title', value)}
96+
VariablePicker={WorkflowVariablePicker}
97+
/>
98+
<FormTextFieldInput
99+
label={t`Description`}
100+
placeholder={t`Enter event description`}
101+
multiline
102+
readonly={actionOptions.readonly}
103+
defaultValue={formData.description}
104+
onChange={(value) => handleFieldChange('description', value)}
105+
VariablePicker={WorkflowVariablePicker}
106+
/>
107+
<FormTextFieldInput
108+
label={t`Location`}
109+
placeholder={t`Enter event location`}
110+
readonly={actionOptions.readonly}
111+
defaultValue={formData.location}
112+
onChange={(value) => handleFieldChange('location', value)}
113+
VariablePicker={WorkflowVariablePicker}
114+
/>
115+
<FormTextFieldInput
116+
label={t`Starts at`}
117+
placeholder={t`ISO 8601 with offset, e.g. 2026-07-01T15:00:00Z`}
118+
readonly={actionOptions.readonly}
119+
defaultValue={formData.startsAt}
120+
onChange={(value) => handleFieldChange('startsAt', value)}
121+
VariablePicker={WorkflowVariablePicker}
122+
/>
123+
<FormTextFieldInput
124+
label={t`Ends at`}
125+
placeholder={t`ISO 8601 with offset, e.g. 2026-07-01T16:00:00Z`}
126+
readonly={actionOptions.readonly}
127+
defaultValue={formData.endsAt}
128+
onChange={(value) => handleFieldChange('endsAt', value)}
129+
VariablePicker={WorkflowVariablePicker}
130+
/>
131+
<FormTextFieldInput
132+
label={t`Time zone`}
133+
placeholder={t`IANA time zone, e.g. America/New_York (defaults to UTC)`}
134+
readonly={actionOptions.readonly}
135+
defaultValue={formData.timeZone}
136+
onChange={(value) => handleFieldChange('timeZone', value)}
137+
VariablePicker={WorkflowVariablePicker}
138+
/>
139+
<FormBooleanFieldToggleInput
140+
label={t`All day`}
141+
description={t`Create the event as an all-day event`}
142+
value={formData.isFullDay}
143+
onChange={(value) => handleFieldChange('isFullDay', value)}
144+
disabled={actionOptions.readonly}
145+
/>
146+
<FormMultiTextFieldInput
147+
label={t`Attendees`}
148+
placeholder={t`Enter emails, comma-separated`}
149+
readonly={actionOptions.readonly}
150+
defaultValue={formData.attendees}
151+
onChange={(value) => handleFieldChange('attendees', value)}
152+
VariablePicker={WorkflowVariablePicker}
153+
/>
154+
<FormBooleanFieldToggleInput
155+
label={t`Send invitations`}
156+
description={t`Email the attendees an invitation`}
157+
hint={t`When off, the event is created with no attendees and nobody is notified.`}
158+
value={formData.sendInvitations}
159+
onChange={(value) => handleFieldChange('sendInvitations', value)}
160+
disabled={actionOptions.readonly}
161+
/>
162+
<FormBooleanFieldToggleInput
163+
label={t`Add conferencing`}
164+
description={t`Add a video conferencing link`}
165+
hint={t`Generates a Google Meet or Microsoft Teams link depending on the account.`}
166+
value={formData.addConferencing}
167+
onChange={(value) => handleFieldChange('addConferencing', value)}
168+
disabled={actionOptions.readonly}
169+
/>
170+
</WorkflowStepBody>
171+
{!actionOptions.readonly && <WorkflowStepFooter stepId={action.id} />}
172+
</>
173+
);
174+
};
Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { type WorkflowActionType } from '@/workflow/types/Workflow';
22
import { CODE_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/CodeAction';
3+
import { CREATE_CALENDAR_EVENT_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/CreateCalendarEventAction';
34
import { DRAFT_EMAIL_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/DraftEmailAction';
45
import { HTTP_REQUEST_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/HttpRequestAction';
56
import { SEND_EMAIL_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/SendEmailAction';
@@ -8,7 +9,17 @@ export const CORE_ACTIONS: Array<{
89
defaultLabel: string;
910
type: Extract<
1011
WorkflowActionType,
11-
'CODE' | 'SEND_EMAIL' | 'DRAFT_EMAIL' | 'HTTP_REQUEST'
12+
| 'CODE'
13+
| 'SEND_EMAIL'
14+
| 'DRAFT_EMAIL'
15+
| 'HTTP_REQUEST'
16+
| 'CREATE_CALENDAR_EVENT'
1217
>;
1318
icon: string;
14-
}> = [SEND_EMAIL_ACTION, DRAFT_EMAIL_ACTION, CODE_ACTION, HTTP_REQUEST_ACTION];
19+
}> = [
20+
SEND_EMAIL_ACTION,
21+
DRAFT_EMAIL_ACTION,
22+
CREATE_CALENDAR_EVENT_ACTION,
23+
CODE_ACTION,
24+
HTTP_REQUEST_ACTION,
25+
];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { type WorkflowActionType } from '@/workflow/types/Workflow';
2+
3+
export const CREATE_CALENDAR_EVENT_ACTION: {
4+
defaultLabel: string;
5+
type: Extract<WorkflowActionType, 'CREATE_CALENDAR_EVENT'>;
6+
icon: string;
7+
} = {
8+
defaultLabel: 'Create Calendar Event',
9+
type: 'CREATE_CALENDAR_EVENT',
10+
icon: 'IconCalendarEvent',
11+
};

0 commit comments

Comments
 (0)