feat(calendly): extend tools with booking, availability, no-shows, and routing forms (#6545)

* feat(calendly): extend tools with booking, availability, no-shows, and routing forms

Adds 12 tools verified against the Calendly OpenAPI spec: get_user,
get_event_invitee, create_event_invitee, list_event_type_available_times,
list_user_busy_times, list_user_availability_schedules,
create_scheduling_link, create/delete_invitee_no_show,
list_organization_memberships, list_routing_forms, and
list_routing_form_submissions.

Also fixes issues found while validating the existing tools:

- list_webhooks dropped the scope query param the API requires, so every
  call with scope unset returned 400
- list_event_types could only send active=true, making inactive event
  types unlistable
- user and organization filters now accept a bare UUID or a full URI
  consistently across every operation
- json array params (eventGuests, events) are normalized whether they
  arrive as an array or a JSON string

* improvement(calendly): type the block/tool alignment test instead of using any

* fix(calendly): normalize webhook organization and user identifiers
This commit is contained in:
Waleed
2026-08-11 11:22:28 -07:00
committed by GitHub
parent 2f1bb5af8b
commit c365b14f73
33 changed files with 2903 additions and 87 deletions
@@ -58,6 +58,36 @@ Get information about the currently authenticated Calendly user
| ↳ `updated_at` | string | ISO timestamp when user was last updated |
| ↳ `current_organization` | string | URI of current organization |
### Calendly Get User
Get information about a specific Calendly user
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Calendly Personal Access Token |
| `userUuid` | string | Yes | User UUID. Format: UUID \(e.g., "abc123-def456"\), full URI \(e.g., "https://api.calendly.com/users/abc123-def456"\), or the constant "me" for the authenticated user |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `resource` | object | User information |
| ↳ `uri` | string | Canonical reference to the user |
| ↳ `name` | string | User full name |
| ↳ `slug` | string | Unique identifier for the user in URLs |
| ↳ `email` | string | User email address |
| ↳ `scheduling_url` | string | URL to the user's scheduling page |
| ↳ `timezone` | string | User timezone |
| ↳ `time_notation` | string | Time notation preference \(12h or 24h\) |
| ↳ `avatar_url` | string | URL to user avatar image |
| ↳ `created_at` | string | ISO timestamp when user was created |
| ↳ `updated_at` | string | ISO timestamp when user was last updated |
| ↳ `current_organization` | string | URI of current organization |
| ↳ `resource_type` | string | Resource type |
| ↳ `locale` | string | User locale |
### Calendly List Event Types
Retrieve a list of all event types for a user or organization
@@ -136,6 +166,29 @@ Get detailed information about a specific event type
| ↳ `type` | string | Event type classification |
| ↳ `updated_at` | string | ISO timestamp of last update |
### Calendly List Event Type Available Times
Retrieve bookable time slots for an event type within a date range
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Calendly Personal Access Token |
| `eventTypeUri` | string | Yes | Event type to check. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/event_types/abc123-def456"\) |
| `startTime` | string | Yes | Start of the availability range. Cannot be in the past. Format: ISO 8601 \(e.g., "2024-01-01T00:00:00Z"\) |
| `endTime` | string | Yes | End of the availability range. Must be in the future and no more than 31 days after the start. Format: ISO 8601 \(e.g., "2024-01-15T00:00:00Z"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `collection` | array | Array of available time slots |
| ↳ `status` | string | Availability status of the slot |
| ↳ `invitees_remaining` | number | Number of invitees that can still book this slot |
| ↳ `start_time` | string | ISO timestamp of the slot start |
| ↳ `scheduling_url` | string | URL that books this exact slot |
### Calendly List Scheduled Events
Retrieve a list of scheduled events for a user or organization
@@ -269,6 +322,108 @@ Retrieve a list of invitees for a scheduled event
| ↳ `next_page_token` | string | Token for next page |
| ↳ `previous_page_token` | string | Token for previous page |
### Calendly Get Event Invitee
Retrieve a single invitee of a scheduled event, including their intake answers
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Calendly Personal Access Token |
| `eventUuid` | string | Yes | Scheduled event UUID. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/scheduled_events/abc123-def456"\) |
| `inviteeUuid` | string | Yes | Invitee UUID. Format: UUID \(e.g., "abc123-def456"\) or full invitee URI \(e.g., "https://api.calendly.com/scheduled_events/abc123/invitees/def456"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `resource` | object | Invitee details |
| ↳ `uri` | string | Canonical reference to the invitee |
| ↳ `email` | string | Invitee email address |
| ↳ `name` | string | Invitee full name |
| ↳ `first_name` | string | Invitee first name |
| ↳ `last_name` | string | Invitee last name |
| ↳ `status` | string | Invitee status \(active or canceled\) |
| ↳ `timezone` | string | Invitee timezone |
| ↳ `event` | string | URI of the scheduled event |
| ↳ `created_at` | string | ISO timestamp when invitee was created |
| ↳ `updated_at` | string | ISO timestamp when invitee was updated |
| ↳ `cancel_url` | string | URL to cancel the booking |
| ↳ `reschedule_url` | string | URL to reschedule the booking |
| ↳ `rescheduled` | boolean | Whether the invitee rescheduled |
| ↳ `text_reminder_number` | string | Phone number used for SMS reminders |
| ↳ `routing_form_submission` | string | URI of the routing form submission that produced this booking |
| ↳ `questions_and_answers` | array | Responses to custom questions |
| ↳ `question` | string | Question text |
| ↳ `answer` | string | Invitee answer |
| ↳ `position` | number | Question order |
| ↳ `tracking` | object | UTM and Salesforce tracking parameters captured at booking |
| ↳ `utm_campaign` | string | UTM campaign |
| ↳ `utm_source` | string | UTM source |
| ↳ `utm_medium` | string | UTM medium |
| ↳ `utm_content` | string | UTM content |
| ↳ `utm_term` | string | UTM term |
| ↳ `salesforce_uuid` | string | Salesforce record identifier |
| ↳ `cancellation` | object | Cancellation details when the invitee has canceled |
| ↳ `canceled_by` | string | Name of person who canceled |
| ↳ `reason` | string | Cancellation reason |
| ↳ `canceler_type` | string | Type of canceler \(host or invitee\) |
| ↳ `created_at` | string | ISO timestamp of the cancellation |
| ↳ `no_show` | object | No-show record when the invitee has been marked as a no-show |
| ↳ `uri` | string | Canonical reference to the no-show |
| ↳ `created_at` | string | ISO timestamp when marked as no-show |
| ↳ `payment` | object | Payment collected at booking |
| ↳ `external_id` | string | Payment identifier at the provider |
| ↳ `provider` | string | Payment provider |
| ↳ `amount` | number | Amount charged |
| ↳ `currency` | string | Currency code |
| ↳ `terms` | string | Payment terms |
| ↳ `successful` | boolean | Whether the payment succeeded |
### Calendly Book Meeting
Book a meeting by creating an invitee on an event type at a chosen time. Requires a paid Calendly plan
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Calendly Personal Access Token |
| `eventTypeUri` | string | Yes | Event type to book. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/event_types/abc123-def456"\) |
| `startTime` | string | Yes | Start time of the booking in UTC. Must be an available slot. Format: ISO 8601 \(e.g., "2024-01-15T09:00:00Z"\) |
| `inviteeEmail` | string | Yes | Email address of the invitee being booked |
| `inviteeTimezone` | string | Yes | Invitee timezone. Format: IANA timezone \(e.g., "America/New_York"\) |
| `inviteeName` | string | No | Full name of the invitee. Required when a first name is not provided |
| `inviteeFirstName` | string | No | First name of the invitee. Required when a full name is not provided |
| `inviteeLastName` | string | No | Last name of the invitee |
| `textReminderNumber` | string | No | Phone number for SMS reminders. Format: E.164 phone number \(e.g., "+14155551234"\) |
| `eventGuests` | json | No | Additional guests to copy on the invite. Format: array of email strings \(max 10, e.g., \["guest@example.com"\]\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `resource` | object | The invitee created for the booking |
| ↳ `uri` | string | Canonical reference to the invitee |
| ↳ `email` | string | Invitee email address |
| ↳ `name` | string | Invitee full name |
| ↳ `first_name` | string | Invitee first name |
| ↳ `last_name` | string | Invitee last name |
| ↳ `status` | string | Invitee status \(active or canceled\) |
| ↳ `timezone` | string | Invitee timezone |
| ↳ `event` | string | URI of the scheduled event that was booked |
| ↳ `created_at` | string | ISO timestamp when the booking was created |
| ↳ `updated_at` | string | ISO timestamp when the booking was updated |
| ↳ `cancel_url` | string | URL to cancel the booking |
| ↳ `reschedule_url` | string | URL to reschedule the booking |
| ↳ `rescheduled` | boolean | Whether the invitee rescheduled |
| ↳ `text_reminder_number` | string | Phone number used for SMS reminders |
| ↳ `questions_and_answers` | array | Responses to custom questions |
| ↳ `question` | string | Question text |
| ↳ `answer` | string | Invitee answer |
| ↳ `position` | number | Question order |
### Calendly Cancel Event
Cancel a scheduled event
@@ -291,6 +446,248 @@ Cancel a scheduled event
| ↳ `reason` | string | Cancellation reason |
| ↳ `created_at` | string | ISO timestamp when event was canceled |
### Calendly Mark Invitee No-Show
Mark an invitee of a scheduled event as a no-show
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Calendly Personal Access Token |
| `inviteeUri` | string | Yes | Full invitee URI to mark as a no-show. Format: URI \(e.g., "https://api.calendly.com/scheduled_events/abc123/invitees/def456"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `resource` | object | The created no-show record |
| ↳ `uri` | string | Canonical reference to the no-show |
| ↳ `invitee` | string | URI of the invitee marked as a no-show |
| ↳ `created_at` | string | ISO timestamp when the no-show was recorded |
### Calendly Unmark Invitee No-Show
Remove the no-show status from an invitee
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Calendly Personal Access Token |
| `noShowUuid` | string | Yes | No-show UUID to remove. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/invitee_no_shows/abc123-def456"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `deleted` | boolean | Whether the no-show status was successfully removed |
| `message` | string | Status message |
### Calendly Create Scheduling Link
Create a single-use scheduling link for an event type
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Calendly Personal Access Token |
| `eventTypeUri` | string | Yes | Event type the link books. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/event_types/abc123-def456"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `resource` | object | The created scheduling link |
| ↳ `booking_url` | string | Single-use URL to share with an invitee |
| ↳ `owner` | string | URI of the event type that owns the link |
| ↳ `owner_type` | string | Resource type of the owner |
### Calendly List User Busy Times
Retrieve a user's internal and external busy times within a date range, based on their connected calendars
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Calendly Personal Access Token |
| `user` | string | Yes | User whose busy times are returned. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/users/abc123-def456"\) |
| `startTime` | string | Yes | Start of the requested range. Cannot be in the past. Format: ISO 8601 \(e.g., "2024-01-01T00:00:00Z"\) |
| `endTime` | string | Yes | End of the requested range. Must be after the start and no more than 7 days later. Format: ISO 8601 \(e.g., "2024-01-07T00:00:00Z"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `collection` | array | Array of busy time blocks |
| ↳ `type` | string | Source of the busy block \(calendly, external, or reserved\) |
| ↳ `start_time` | string | ISO timestamp when the block starts |
| ↳ `end_time` | string | ISO timestamp when the block ends |
| ↳ `buffered_start_time` | string | ISO timestamp when the block starts including buffer |
| ↳ `buffered_end_time` | string | ISO timestamp when the block ends including buffer |
| ↳ `event` | object | The Calendly event occupying this block |
| ↳ `uri` | string | URI of the scheduled event |
### Calendly List User Availability Schedules
Retrieve a user's availability schedules, working hours, and date overrides
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Calendly Personal Access Token |
| `user` | string | Yes | User whose schedules are returned. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/users/abc123-def456"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `collection` | array | Array of availability schedules |
| ↳ `uri` | string | Canonical reference to the schedule |
| ↳ `name` | string | Schedule name |
| ↳ `default` | boolean | Whether this is the user's default schedule |
| ↳ `user` | string | URI of the owning user |
| ↳ `timezone` | string | Timezone the schedule is defined in |
| ↳ `rules` | array | Weekly rules and date overrides that make up the schedule |
| ↳ `type` | string | Rule type \(wday or date\) |
| ↳ `wday` | string | Day of week the rule applies to, for wday rules |
| ↳ `date` | string | Calendar date the rule overrides, for date rules |
| ↳ `intervals` | array | Available intervals for the rule; empty means unavailable |
| ↳ `from` | string | Interval start time \(HH:MM\) |
| ↳ `to` | string | Interval end time \(HH:MM\) |
### Calendly List Organization Memberships
Retrieve the members of an organization, including each member profile and role
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Calendly Personal Access Token |
| `organization` | string | No | Return memberships that belong to this organization. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/organizations/abc123-def456"\) |
| `user` | string | No | Return memberships that belong to this user. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/users/abc123-def456"\) |
| `email` | string | No | Filter memberships by member email address |
| `role` | string | No | Filter by role. Format: "owner", "admin", or "user" |
| `count` | number | No | Number of results per page. Format: integer \(default: 20, max: 100\) |
| `pageToken` | string | No | Page token for pagination. Format: opaque string from previous response next_page_token |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `collection` | array | Array of organization membership objects |
| ↳ `uri` | string | Canonical reference to the membership |
| ↳ `role` | string | Member role \(owner, admin, or user\) |
| ↳ `organization` | string | URI of the organization |
| ↳ `created_at` | string | ISO timestamp when the member joined |
| ↳ `updated_at` | string | ISO timestamp when the membership changed |
| ↳ `user` | object | The member |
| ↳ `uri` | string | Canonical reference to the user |
| ↳ `name` | string | User full name |
| ↳ `slug` | string | Unique identifier for the user in URLs |
| ↳ `email` | string | User email address |
| ↳ `scheduling_url` | string | URL to the user's scheduling page |
| ↳ `timezone` | string | User timezone |
| ↳ `time_notation` | string | Time notation preference \(12h or 24h\) |
| ↳ `avatar_url` | string | URL to user avatar image |
| ↳ `locale` | string | User locale |
| ↳ `created_at` | string | ISO timestamp when user was created |
| ↳ `updated_at` | string | ISO timestamp when user was updated |
| `pagination` | object | Pagination information |
| ↳ `count` | number | Number of results in this page |
| ↳ `next_page` | string | URL to next page \(if available\) |
| ↳ `previous_page` | string | URL to previous page \(if available\) |
| ↳ `next_page_token` | string | Token for next page |
| ↳ `previous_page_token` | string | Token for previous page |
### Calendly List Routing Forms
Retrieve the routing forms of an organization, including their questions
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Calendly Personal Access Token |
| `organization` | string | Yes | Organization whose routing forms are returned. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/organizations/abc123-def456"\) |
| `count` | number | No | Number of results per page. Format: integer \(default: 20, max: 100\) |
| `pageToken` | string | No | Page token for pagination. Format: opaque string from previous response next_page_token |
| `sort` | string | No | Sort order for results. Format: "created_at:direction" \(e.g., "created_at:asc", "created_at:desc"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `collection` | array | Array of routing form objects |
| ↳ `uri` | string | Canonical reference to the routing form |
| ↳ `organization` | string | URI of the owning organization |
| ↳ `name` | string | Routing form name |
| ↳ `status` | string | Routing form status \(published or draft\) |
| ↳ `created_at` | string | ISO timestamp when the form was created |
| ↳ `updated_at` | string | ISO timestamp when the form was updated |
| ↳ `questions` | array | Questions asked by the routing form |
| ↳ `uuid` | string | Question identifier |
| ↳ `name` | string | Question text |
| ↳ `type` | string | Question answer type |
| ↳ `required` | boolean | Whether an answer is required |
| ↳ `answer_choices` | array | Selectable answers for choice questions |
| `pagination` | object | Pagination information |
| ↳ `count` | number | Number of results in this page |
| ↳ `next_page` | string | URL to next page \(if available\) |
| ↳ `previous_page` | string | URL to previous page \(if available\) |
| ↳ `next_page_token` | string | Token for next page |
| ↳ `previous_page_token` | string | Token for previous page |
### Calendly List Routing Form Submissions
Retrieve the submissions of a routing form, including answers and routing result
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Calendly Personal Access Token |
| `formUri` | string | Yes | Routing form whose submissions are returned. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/routing_forms/abc123-def456"\) |
| `count` | number | No | Number of results per page. Format: integer \(default: 20, max: 100\) |
| `pageToken` | string | No | Page token for pagination. Format: opaque string from previous response next_page_token |
| `sort` | string | No | Sort order for results. Format: "created_at:direction" \(e.g., "created_at:asc", "created_at:desc"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `collection` | array | Array of routing form submission objects |
| ↳ `uri` | string | Canonical reference to the submission |
| ↳ `routing_form` | string | URI of the routing form |
| ↳ `submitter` | string | URI of the invitee who submitted, when the submission led to a booking |
| ↳ `submitter_type` | string | Type of the submitter |
| ↳ `created_at` | string | ISO timestamp when the form was submitted |
| ↳ `updated_at` | string | ISO timestamp when the submission was updated |
| ↳ `questions_and_answers` | array | Answers given on the routing form |
| ↳ `question_uuid` | string | Question identifier |
| ↳ `question` | string | Question text |
| ↳ `answer` | string | Submitted answer |
| ↳ `tracking` | object | UTM and Salesforce tracking parameters captured at submission |
| ↳ `utm_campaign` | string | UTM campaign |
| ↳ `utm_source` | string | UTM source |
| ↳ `utm_medium` | string | UTM medium |
| ↳ `utm_content` | string | UTM content |
| ↳ `utm_term` | string | UTM term |
| ↳ `salesforce_uuid` | string | Salesforce record identifier |
| ↳ `result` | object | Where the submission routed to |
| ↳ `type` | string | Routing result type |
| ↳ `value` | string | Routing destination |
| `pagination` | object | Pagination information |
| ↳ `count` | number | Number of results in this page |
| ↳ `next_page` | string | URL to next page \(if available\) |
| ↳ `previous_page` | string | URL to previous page \(if available\) |
| ↳ `next_page_token` | string | Token for next page |
| ↳ `previous_page_token` | string | Token for previous page |
## Triggers
+105
View File
@@ -0,0 +1,105 @@
/**
* Guards the block/tool contract: the operation dropdown, `tools.access`, the tool params, and the
* declared inputs all describe the same set of operations, and drift in any one of them fails here.
*
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { CalendlyBlock } from '@/blocks/blocks/calendly'
import type { SubBlockConfig } from '@/blocks/types'
import * as calendlyTools from '@/tools/calendly'
import type { ToolConfig } from '@/tools/types'
const TRIGGER_FIELDS = new Set(['operation', 'selectedTriggerId'])
const toolsById = new Map<string, ToolConfig>(
Object.values(calendlyTools).map((tool) => [tool.id, tool])
)
const subBlocks: SubBlockConfig[] = CalendlyBlock.subBlocks
const access: string[] = CalendlyBlock.tools.access ?? []
const declaredInputs = Object.keys(CalendlyBlock.inputs ?? {})
const operationSubBlock = subBlocks.find((subBlock) => subBlock.id === 'operation')
const operationOptions =
typeof operationSubBlock?.options === 'function'
? operationSubBlock.options()
: (operationSubBlock?.options ?? [])
const operations = operationOptions.map((option) => option.id)
/**
* Trigger mode re-declares its own credential fields keyed on `selectedTriggerId`, so only the
* operation-gated subblocks describe the tool surface.
*/
const operationSubBlocks = subBlocks.filter((subBlock) => {
const condition = subBlock.condition
if (typeof condition === 'function') return false
return subBlock.id === 'operation' || !condition || condition.field === 'operation'
})
function visibleFor(operation: string): string[] {
return operationSubBlocks
.filter((subBlock) => {
const condition = subBlock.condition
if (typeof condition === 'function' || !condition) return true
return Array.isArray(condition.value)
? condition.value.includes(operation)
: condition.value === operation
})
.map((subBlock) => subBlock.id)
}
describe('calendly block/tool alignment', () => {
it('maps every operation to a registered tool and back', () => {
expect(operations.filter((operation) => !access.includes(operation))).toEqual([])
expect(access.filter((tool) => !operations.includes(tool))).toEqual([])
expect(access.filter((tool) => !toolsById.has(tool))).toEqual([])
})
it('keeps operation-gated subBlock ids unique', () => {
const ids = operationSubBlocks.map((subBlock) => subBlock.id)
expect(ids.filter((id, index) => ids.indexOf(id) !== index)).toEqual([])
})
it('exposes a subBlock for every required tool param', () => {
const missing: string[] = []
for (const operation of operations) {
const tool = toolsById.get(operation)
if (!tool) continue
const visible = visibleFor(operation)
for (const [name, param] of Object.entries(tool.params)) {
if (param.visibility === 'hidden' || !param.required) continue
if (!visible.includes(name)) missing.push(`${operation}.${name}`)
}
}
expect(missing).toEqual([])
})
it('backs every visible subBlock with a param on its operation tool', () => {
const stray: string[] = []
for (const operation of operations) {
const tool = toolsById.get(operation)
if (!tool) continue
for (const id of visibleFor(operation)) {
if (TRIGGER_FIELDS.has(id)) continue
if (!tool.params[id]) stray.push(`${operation}.${id}`)
}
}
expect(stray).toEqual([])
})
it('declares every operation-gated subBlock in block inputs', () => {
const undeclared = operationSubBlocks
.filter((subBlock) => !TRIGGER_FIELDS.has(subBlock.id) && subBlock.condition)
.map((subBlock) => subBlock.id)
.filter((id) => !declaredInputs.includes(id))
expect(undeclared).toEqual([])
})
})
+364 -36
View File
@@ -1,3 +1,4 @@
import { getErrorMessage } from '@sim/utils/errors'
import { CalendlyIcon } from '@/components/icons'
import type { BlockConfig, BlockMeta } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
@@ -10,6 +11,20 @@ import { getTrigger } from '@/triggers'
*/
const SCOPE_FIELD = ['user', 'organization'] as const
/**
* A `json` field arrives as text the user typed, but as a real array when an upstream block is
* referenced, so only the text form is parsed.
*/
function parseJsonArray(value: unknown, field: string): unknown {
if (typeof value !== 'string') return value
try {
return JSON.parse(value)
} catch (error) {
throw new Error(`Invalid JSON input for ${field}: ${getErrorMessage(error)}`)
}
}
export const CalendlyBlock: BlockConfig<ToolResponse> = {
type: 'calendly',
name: 'Calendly',
@@ -33,8 +48,14 @@ export const CalendlyBlock: BlockConfig<ToolResponse> = {
sentences: {
byOperation: {
calendly_get_current_user: ['Read the connected account profile'],
calendly_get_user: [{ text: 'Read user', field: 'userUuid', core: true }],
calendly_list_event_types: ['List event types', { text: ', for', field: SCOPE_FIELD }],
calendly_get_event_type: [{ text: 'Read event type', field: 'eventTypeUuid', core: true }],
calendly_list_event_type_available_times: [
{ text: 'List open slots for event type', field: 'eventTypeUri', core: true },
{ text: ', from', field: 'startTime' },
{ text: ', until', field: 'endTime' },
],
calendly_list_scheduled_events: [
'List scheduled events',
{ text: ', for', field: SCOPE_FIELD },
@@ -49,10 +70,48 @@ export const CalendlyBlock: BlockConfig<ToolResponse> = {
{ text: ', matching', field: 'email' },
{ text: ', with status', field: 'status' },
],
calendly_get_event_invitee: [
{ text: 'Read invitee', field: 'inviteeUuid', core: true },
{ text: ', of event', field: 'eventUuid' },
],
calendly_create_event_invitee: [
{ text: 'Book event type', field: 'eventTypeUri', core: true },
{ text: ', for', field: 'inviteeEmail' },
{ text: ', at', field: 'startTime' },
],
calendly_cancel_event: [
{ text: 'Cancel event', field: 'eventUuid', core: true },
{ text: ', with reason', field: 'reason' },
],
calendly_create_invitee_no_show: [
{ text: 'Mark invitee as a no-show', field: 'inviteeUri', core: true },
],
calendly_delete_invitee_no_show: [
{ text: 'Remove no-show', field: 'noShowUuid', core: true },
],
calendly_create_scheduling_link: [
{ text: 'Create a single-use link for event type', field: 'eventTypeUri', core: true },
],
calendly_list_user_busy_times: [
{ text: 'List busy times for', field: 'user', core: true },
{ text: ', from', field: 'startTime' },
{ text: ', until', field: 'endTime' },
],
calendly_list_user_availability_schedules: [
{ text: 'List availability schedules for', field: 'user', core: true },
],
calendly_list_organization_memberships: [
'List organization members',
{ text: ', in', field: 'organization' },
{ text: ', with role', field: 'role' },
{ text: ', matching', field: 'email' },
],
calendly_list_routing_forms: [
{ text: 'List routing forms of', field: 'organization', core: true },
],
calendly_list_routing_form_submissions: [
{ text: 'List submissions of routing form', field: 'formUri', core: true },
],
},
},
},
@@ -63,12 +122,30 @@ export const CalendlyBlock: BlockConfig<ToolResponse> = {
type: 'dropdown',
options: [
{ label: 'Get Current User', id: 'calendly_get_current_user' },
{ label: 'Get User', id: 'calendly_get_user' },
{ label: 'List Event Types', id: 'calendly_list_event_types' },
{ label: 'Get Event Type', id: 'calendly_get_event_type' },
{
label: 'List Event Type Available Times',
id: 'calendly_list_event_type_available_times',
},
{ label: 'List Scheduled Events', id: 'calendly_list_scheduled_events' },
{ label: 'Get Scheduled Event', id: 'calendly_get_scheduled_event' },
{ label: 'List Event Invitees', id: 'calendly_list_event_invitees' },
{ label: 'Get Event Invitee', id: 'calendly_get_event_invitee' },
{ label: 'Book Meeting', id: 'calendly_create_event_invitee' },
{ label: 'Cancel Event', id: 'calendly_cancel_event' },
{ label: 'Mark Invitee No-Show', id: 'calendly_create_invitee_no_show' },
{ label: 'Unmark Invitee No-Show', id: 'calendly_delete_invitee_no_show' },
{ label: 'Create Scheduling Link', id: 'calendly_create_scheduling_link' },
{ label: 'List User Busy Times', id: 'calendly_list_user_busy_times' },
{
label: 'List User Availability Schedules',
id: 'calendly_list_user_availability_schedules',
},
{ label: 'List Organization Memberships', id: 'calendly_list_organization_memberships' },
{ label: 'List Routing Forms', id: 'calendly_list_routing_forms' },
{ label: 'List Routing Form Submissions', id: 'calendly_list_routing_form_submissions' },
],
value: () => 'calendly_list_scheduled_events',
},
@@ -80,7 +157,14 @@ export const CalendlyBlock: BlockConfig<ToolResponse> = {
password: true,
required: true,
},
// Get Event Type fields
{
id: 'userUuid',
title: 'User UUID',
type: 'short-input',
placeholder: 'Enter user UUID or URI, or "me"',
required: true,
condition: { field: 'operation', value: 'calendly_get_user' },
},
{
id: 'eventTypeUuid',
title: 'Event Type UUID',
@@ -89,25 +173,55 @@ export const CalendlyBlock: BlockConfig<ToolResponse> = {
required: true,
condition: { field: 'operation', value: 'calendly_get_event_type' },
},
// List Event Types fields
{
id: 'eventTypeUri',
title: 'Event Type',
type: 'short-input',
placeholder: 'Enter event type UUID or URI',
required: true,
condition: {
field: 'operation',
value: [
'calendly_list_event_type_available_times',
'calendly_create_event_invitee',
'calendly_create_scheduling_link',
],
},
},
{
id: 'user',
title: 'User URI',
type: 'short-input',
placeholder: 'Filter by user URI',
placeholder: 'Enter user UUID or URI',
required: {
field: 'operation',
value: ['calendly_list_user_busy_times', 'calendly_list_user_availability_schedules'],
},
condition: {
field: 'operation',
value: ['calendly_list_event_types', 'calendly_list_scheduled_events'],
value: [
'calendly_list_event_types',
'calendly_list_scheduled_events',
'calendly_list_user_busy_times',
'calendly_list_user_availability_schedules',
'calendly_list_organization_memberships',
],
},
},
{
id: 'organization',
title: 'Organization URI',
type: 'short-input',
placeholder: 'Filter by organization URI (optional)',
placeholder: 'Enter organization UUID or URI',
required: { field: 'operation', value: 'calendly_list_routing_forms' },
condition: {
field: 'operation',
value: ['calendly_list_event_types', 'calendly_list_scheduled_events'],
value: [
'calendly_list_event_types',
'calendly_list_scheduled_events',
'calendly_list_organization_memberships',
'calendly_list_routing_forms',
],
},
},
{
@@ -118,7 +232,6 @@ export const CalendlyBlock: BlockConfig<ToolResponse> = {
'When enabled, shows only active event types. When disabled, shows all event types.',
condition: { field: 'operation', value: 'calendly_list_event_types' },
},
// List Scheduled Events fields
{
id: 'invitee_email',
title: 'Invitee Email',
@@ -130,6 +243,7 @@ export const CalendlyBlock: BlockConfig<ToolResponse> = {
id: 'min_start_time',
title: 'Min Start Time',
type: 'short-input',
mode: 'advanced',
placeholder: 'ISO 8601 format (e.g., 2024-01-01T00:00:00Z)',
condition: { field: 'operation', value: 'calendly_list_scheduled_events' },
wandConfig: {
@@ -151,6 +265,7 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes
id: 'max_start_time',
title: 'Max Start Time',
type: 'short-input',
mode: 'advanced',
placeholder: 'ISO 8601 format (e.g., 2024-12-31T23:59:59Z)',
condition: { field: 'operation', value: 'calendly_list_scheduled_events' },
wandConfig: {
@@ -182,7 +297,60 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes
value: ['calendly_list_scheduled_events', 'calendly_list_event_invitees'],
},
},
// Get Scheduled Event / List Invitees / Cancel Event fields
{
id: 'startTime',
title: 'Start Time',
type: 'short-input',
placeholder: 'ISO 8601 format (e.g., 2024-01-01T00:00:00Z)',
required: true,
condition: {
field: 'operation',
value: [
'calendly_list_event_type_available_times',
'calendly_list_user_busy_times',
'calendly_create_event_invitee',
],
},
wandConfig: {
enabled: true,
prompt: `Generate an ISO 8601 timestamp based on the user's description.
The timestamp should be in the format: YYYY-MM-DDTHH:MM:SSZ (UTC timezone).
Examples:
- "today" -> Today's date at 00:00:00Z
- "tomorrow morning" -> Tomorrow's date at 09:00:00Z
- "start of next week" -> Monday of next week at 00:00:00Z
Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes, no extra text.`,
placeholder: 'Describe the start time (e.g., "today", "tomorrow morning")...',
generationType: 'timestamp',
},
},
{
id: 'endTime',
title: 'End Time',
type: 'short-input',
placeholder: 'ISO 8601 format (e.g., 2024-01-07T00:00:00Z)',
description:
'Busy times allow a range of up to 7 days. Available times allow a range of up to 31 days.',
required: true,
condition: {
field: 'operation',
value: ['calendly_list_event_type_available_times', 'calendly_list_user_busy_times'],
},
wandConfig: {
enabled: true,
prompt: `Generate an ISO 8601 timestamp based on the user's description.
The timestamp should be in the format: YYYY-MM-DDTHH:MM:SSZ (UTC timezone).
Examples:
- "end of today" -> Today's date at 23:59:59Z
- "in 7 days" -> 7 days from now at 23:59:59Z
- "end of this week" -> Sunday of the current week at 23:59:59Z
Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes, no extra text.`,
placeholder: 'Describe the end time (e.g., "in 7 days", "end of week")...',
generationType: 'timestamp',
},
},
{
id: 'eventUuid',
title: 'Event UUID',
@@ -194,11 +362,19 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes
value: [
'calendly_get_scheduled_event',
'calendly_list_event_invitees',
'calendly_get_event_invitee',
'calendly_cancel_event',
],
},
},
// Cancel Event fields
{
id: 'inviteeUuid',
title: 'Invitee UUID',
type: 'short-input',
placeholder: 'Enter invitee UUID or URI',
required: true,
condition: { field: 'operation', value: 'calendly_get_event_invitee' },
},
{
id: 'reason',
title: 'Cancellation Reason',
@@ -206,19 +382,121 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes
placeholder: 'Reason for cancellation (optional)',
condition: { field: 'operation', value: 'calendly_cancel_event' },
},
// List Event Invitees fields
{
id: 'inviteeEmail',
title: 'Invitee Email',
type: 'short-input',
placeholder: 'Email address of the person being booked',
required: true,
condition: { field: 'operation', value: 'calendly_create_event_invitee' },
},
{
id: 'inviteeName',
title: 'Invitee Name',
type: 'short-input',
placeholder: 'Full name of the invitee',
description: 'Provide a full name or a first name.',
condition: { field: 'operation', value: 'calendly_create_event_invitee' },
},
{
id: 'inviteeTimezone',
title: 'Invitee Timezone',
type: 'short-input',
placeholder: 'IANA timezone (e.g., America/New_York)',
required: true,
condition: { field: 'operation', value: 'calendly_create_event_invitee' },
},
{
id: 'inviteeFirstName',
title: 'Invitee First Name',
type: 'short-input',
placeholder: 'First name of the invitee',
mode: 'advanced',
condition: { field: 'operation', value: 'calendly_create_event_invitee' },
},
{
id: 'inviteeLastName',
title: 'Invitee Last Name',
type: 'short-input',
placeholder: 'Last name of the invitee',
mode: 'advanced',
condition: { field: 'operation', value: 'calendly_create_event_invitee' },
},
{
id: 'textReminderNumber',
title: 'Text Reminder Number',
type: 'short-input',
placeholder: 'E.164 phone number (e.g., +14155551234)',
mode: 'advanced',
condition: { field: 'operation', value: 'calendly_create_event_invitee' },
},
{
id: 'eventGuests',
title: 'Event Guests',
type: 'long-input',
placeholder: '["guest@example.com"]',
description: 'Additional guests to copy on the invite. Maximum of 10.',
mode: 'advanced',
condition: { field: 'operation', value: 'calendly_create_event_invitee' },
wandConfig: {
enabled: true,
prompt:
'Generate a JSON array of guest email address strings, for example ["guest@example.com"]. Return ONLY the JSON array - no explanations, no extra text.',
placeholder: 'Describe the guests to copy on the invite...',
generationType: 'json-object',
},
},
{
id: 'inviteeUri',
title: 'Invitee URI',
type: 'short-input',
placeholder: 'https://api.calendly.com/scheduled_events/.../invitees/...',
required: true,
condition: { field: 'operation', value: 'calendly_create_invitee_no_show' },
},
{
id: 'noShowUuid',
title: 'No-Show UUID',
type: 'short-input',
placeholder: 'Enter no-show UUID or URI',
required: true,
condition: { field: 'operation', value: 'calendly_delete_invitee_no_show' },
},
{
id: 'role',
title: 'Role',
type: 'dropdown',
options: [
{ label: 'All', id: '' },
{ label: 'Owner', id: 'owner' },
{ label: 'Admin', id: 'admin' },
{ label: 'User', id: 'user' },
],
condition: { field: 'operation', value: 'calendly_list_organization_memberships' },
},
{
id: 'formUri',
title: 'Routing Form',
type: 'short-input',
placeholder: 'Enter routing form UUID or URI',
required: true,
condition: { field: 'operation', value: 'calendly_list_routing_form_submissions' },
},
{
id: 'email',
title: 'Email',
type: 'short-input',
placeholder: 'Filter by invitee email',
condition: { field: 'operation', value: 'calendly_list_event_invitees' },
condition: {
field: 'operation',
value: ['calendly_list_event_invitees', 'calendly_list_organization_memberships'],
},
},
// Pagination fields
{
id: 'count',
title: 'Results Per Page',
type: 'short-input',
mode: 'advanced',
placeholder: 'Number of results (default: 20, max: 100)',
condition: {
field: 'operation',
@@ -226,6 +504,9 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes
'calendly_list_event_types',
'calendly_list_scheduled_events',
'calendly_list_event_invitees',
'calendly_list_organization_memberships',
'calendly_list_routing_forms',
'calendly_list_routing_form_submissions',
],
},
},
@@ -233,6 +514,7 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes
id: 'pageToken',
title: 'Page Token',
type: 'short-input',
mode: 'advanced',
placeholder: 'Token for pagination',
condition: {
field: 'operation',
@@ -240,6 +522,9 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes
'calendly_list_event_types',
'calendly_list_scheduled_events',
'calendly_list_event_invitees',
'calendly_list_organization_memberships',
'calendly_list_routing_forms',
'calendly_list_routing_form_submissions',
],
},
},
@@ -247,6 +532,7 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes
id: 'sort',
title: 'Sort Order',
type: 'short-input',
mode: 'advanced',
placeholder: 'e.g., "name:asc", "start_time:desc"',
condition: {
field: 'operation',
@@ -254,10 +540,11 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes
'calendly_list_event_types',
'calendly_list_scheduled_events',
'calendly_list_event_invitees',
'calendly_list_routing_forms',
'calendly_list_routing_form_submissions',
],
},
},
// Trigger SubBlocks
...getTrigger('calendly_invitee_created').subBlocks,
...getTrigger('calendly_invitee_canceled').subBlocks,
...getTrigger('calendly_routing_form_submitted').subBlocks,
@@ -266,31 +553,40 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes
tools: {
access: [
'calendly_get_current_user',
'calendly_get_user',
'calendly_list_event_types',
'calendly_get_event_type',
'calendly_list_event_type_available_times',
'calendly_list_scheduled_events',
'calendly_get_scheduled_event',
'calendly_list_event_invitees',
'calendly_get_event_invitee',
'calendly_create_event_invitee',
'calendly_cancel_event',
'calendly_create_invitee_no_show',
'calendly_delete_invitee_no_show',
'calendly_create_scheduling_link',
'calendly_list_user_busy_times',
'calendly_list_user_availability_schedules',
'calendly_list_organization_memberships',
'calendly_list_routing_forms',
'calendly_list_routing_form_submissions',
],
config: {
tool: (params) => {
return params.operation || 'calendly_list_scheduled_events'
},
params: (params) => {
const { operation, events, ...rest } = params
let parsedEvents: any | undefined
try {
if (events) parsedEvents = JSON.parse(events)
} catch (error: any) {
throw new Error(`Invalid JSON input for events: ${error.message}`)
}
const { operation, events, eventGuests, active, ...rest } = params
return {
...rest,
...(parsedEvents && { events: parsedEvents }),
// The switch cannot express "inactive only", so off means unfiltered, not `active=false`.
...(active === true && { active: true }),
...(events !== undefined && { events: parseJsonArray(events, 'events') }),
...(eventGuests !== undefined && {
eventGuests: parseJsonArray(eventGuests, 'eventGuests'),
}),
}
},
},
@@ -298,26 +594,36 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
apiKey: { type: 'string', description: 'Personal access token' },
// Event Type params
userUuid: { type: 'string', description: 'User UUID' },
eventTypeUuid: { type: 'string', description: 'Event type UUID' },
eventTypeUri: { type: 'string', description: 'Event type UUID or URI' },
user: { type: 'string', description: 'User URI filter' },
organization: { type: 'string', description: 'Organization URI' },
active: { type: 'boolean', description: 'Filter by active status' },
// Scheduled Event params
startTime: { type: 'string', description: 'Start of the requested range (ISO 8601)' },
endTime: { type: 'string', description: 'End of the requested range (ISO 8601)' },
eventUuid: { type: 'string', description: 'Scheduled event UUID' },
invitee_email: { type: 'string', description: 'Filter by invitee email' },
min_start_time: { type: 'string', description: 'Minimum start time (ISO 8601)' },
max_start_time: { type: 'string', description: 'Maximum start time (ISO 8601)' },
status: { type: 'string', description: 'Status filter (active or canceled)' },
// Cancel Event params
reason: { type: 'string', description: 'Cancellation reason' },
// Invitees params
email: { type: 'string', description: 'Filter by email' },
// Pagination params
inviteeUuid: { type: 'string', description: 'Invitee UUID' },
inviteeUri: { type: 'string', description: 'Invitee URI' },
noShowUuid: { type: 'string', description: 'No-show UUID' },
inviteeEmail: { type: 'string', description: 'Email address of the invitee being booked' },
inviteeName: { type: 'string', description: 'Full name of the invitee' },
inviteeFirstName: { type: 'string', description: 'First name of the invitee' },
inviteeLastName: { type: 'string', description: 'Last name of the invitee' },
inviteeTimezone: { type: 'string', description: 'Timezone of the invitee' },
textReminderNumber: { type: 'string', description: 'Phone number for SMS reminders' },
eventGuests: { type: 'json', description: 'Array of additional guest email addresses' },
role: { type: 'string', description: 'Filter memberships by role' },
formUri: { type: 'string', description: 'Routing form UUID or URI' },
count: { type: 'number', description: 'Results per page' },
pageToken: { type: 'string', description: 'Pagination token' },
sort: { type: 'string', description: 'Sort order' },
// Webhook params
webhookUuid: { type: 'string', description: 'Webhook UUID' },
url: { type: 'string', description: 'Webhook callback URL' },
events: { type: 'json', description: 'Array of event types' },
@@ -325,14 +631,10 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes
signing_key: { type: 'string', description: 'Webhook signing key' },
},
outputs: {
// Common outputs
success: { type: 'boolean', description: 'Whether operation succeeded' },
// User outputs
resource: { type: 'json', description: 'Resource data (user, event type, event, etc.)' },
// List outputs
collection: { type: 'json', description: 'Array of items' },
pagination: { type: 'json', description: 'Pagination information' },
// Event details
uri: { type: 'string', description: 'Resource URI' },
name: { type: 'string', description: 'Resource name' },
email: { type: 'string', description: 'Email address' },
@@ -341,13 +643,11 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes
end_time: { type: 'string', description: 'Event end time (ISO 8601)' },
location: { type: 'json', description: 'Event location details' },
scheduling_url: { type: 'string', description: 'Scheduling page URL' },
// Webhook outputs
booking_url: { type: 'string', description: 'Single-use scheduling link' },
callback_url: { type: 'string', description: 'Webhook URL' },
signing_key: { type: 'string', description: 'Webhook signing key' },
// Delete outputs
deleted: { type: 'boolean', description: 'Whether deletion succeeded' },
message: { type: 'string', description: 'Status message' },
// Trigger outputs
event: { type: 'string', description: 'Webhook event type' },
created_at: { type: 'string', description: 'Webhook event creation timestamp' },
created_by: {
@@ -442,6 +742,34 @@ export const CalendlyBlockMeta = {
},
],
skills: [
{
name: 'book-a-meeting',
description:
'Find an open slot on a Calendly event type and book an invitee into it. Use to schedule a meeting end to end without sending the invitee a link.',
content:
'# Book a Meeting\n\nSchedule a Calendly meeting directly.\n\n## Steps\n1. Use List Event Types to find the event type to book, and take its URI.\n2. Use List Event Type Available Times with that event type and a start/end window (ISO 8601 UTC, at most 31 days apart) to get open slots.\n3. Pick a slot and use Book Meeting with the event type, the slot start time, and the invitee email, timezone, and name. Add event guests to copy others on the invite.\n\n## Output\nReturn the booked start time, the invitee, and the reschedule and cancel URLs. Booking requires a paid Calendly plan; if the call is rejected as forbidden, say so rather than retrying. If no slot matches the request, report the nearest available times instead of booking a different one.',
},
{
name: 'check-availability',
description:
"Read a Calendly user's busy times and working hours for a date range. Use to find when someone is free before proposing times.",
content:
"# Check Availability\n\nInspect when a host is free.\n\n## Steps\n1. Get the user URI (Get Current User for yourself, or List Organization Memberships to find a teammate by email).\n2. Use List User Busy Times with that user and a start/end window (ISO 8601 UTC, at most 7 days apart) to get booked and externally blocked time.\n3. Use List User Availability Schedules to read the working hours and date overrides the busy times sit inside.\n\n## Output\nReturn the free windows implied by the working hours minus the busy blocks, in the schedule's timezone. Note which busy blocks are Calendly events versus external calendar holds. Both endpoints cap the range, so split a longer request into several calls rather than widening the window.",
},
{
name: 'share-a-single-use-link',
description:
'Create a one-time Calendly booking link for an event type. Use to invite someone to schedule without exposing a reusable link.',
content:
'# Share a Single-Use Link\n\nHand out a booking link that works once.\n\n## Steps\n1. Use List Event Types to find the event type and take its URI.\n2. Use Create Scheduling Link with that event type.\n3. Send the returned booking URL to the invitee.\n\n## Output\nReturn the booking URL and the event type it books. Make clear the link expires after one booking, so generate a new one per invitee rather than reusing it.',
},
{
name: 'mark-a-no-show',
description:
'Flag a Calendly invitee who did not attend, or clear that flag. Use when reconciling attendance after a meeting.',
content:
'# Mark a No-Show\n\nRecord attendance for a past meeting.\n\n## Steps\n1. Use List Event Invitees for the scheduled event and take the URI of the invitee who did not attend.\n2. Use Mark Invitee No-Show with that invitee URI.\n3. To reverse it, take the no-show URI from the invitee record and use Unmark Invitee No-Show.\n\n## Output\nReturn which invitees were marked and the no-show URI for each, so the flag can be reversed later. Only mark meetings that have already ended.',
},
{
name: 'list-upcoming-meetings',
description:
+49 -1
View File
@@ -2842,6 +2842,10 @@
"name": "Get Current User",
"description": "Get information about the currently authenticated Calendly user"
},
{
"name": "Get User",
"description": "Get information about a specific Calendly user"
},
{
"name": "List Event Types",
"description": "Retrieve a list of all event types for a user or organization"
@@ -2850,6 +2854,10 @@
"name": "Get Event Type",
"description": "Get detailed information about a specific event type"
},
{
"name": "List Event Type Available Times",
"description": "Retrieve bookable time slots for an event type within a date range"
},
{
"name": "List Scheduled Events",
"description": "Retrieve a list of scheduled events for a user or organization"
@@ -2862,12 +2870,52 @@
"name": "List Event Invitees",
"description": "Retrieve a list of invitees for a scheduled event"
},
{
"name": "Get Event Invitee",
"description": "Retrieve a single invitee of a scheduled event, including their intake answers"
},
{
"name": "Book Meeting",
"description": "Book a meeting by creating an invitee on an event type at a chosen time. Requires a paid Calendly plan"
},
{
"name": "Cancel Event",
"description": "Cancel a scheduled event"
},
{
"name": "Mark Invitee No-Show",
"description": "Mark an invitee of a scheduled event as a no-show"
},
{
"name": "Unmark Invitee No-Show",
"description": "Remove the no-show status from an invitee"
},
{
"name": "Create Scheduling Link",
"description": "Create a single-use scheduling link for an event type"
},
{
"name": "List User Busy Times",
"description": "Retrieve a user's internal and external busy times within a date range, based on their connected calendars"
},
{
"name": "List User Availability Schedules",
"description": "Retrieve a user's availability schedules, working hours, and date overrides"
},
{
"name": "List Organization Memberships",
"description": "Retrieve the members of an organization, including each member profile and role"
},
{
"name": "List Routing Forms",
"description": "Retrieve the routing forms of an organization, including their questions"
},
{
"name": "List Routing Form Submissions",
"description": "Retrieve the submissions of a routing form, including answers and routing result"
}
],
"operationCount": 7,
"operationCount": 19,
"triggers": [
{
"id": "calendly_invitee_created",
+182
View File
@@ -0,0 +1,182 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { createEventInviteeTool } from '@/tools/calendly/create_event_invitee'
import { createInviteeNoShowTool } from '@/tools/calendly/create_invitee_no_show'
import { createWebhookTool } from '@/tools/calendly/create_webhook'
import { getEventInviteeTool } from '@/tools/calendly/get_event_invitee'
import { listEventTypesTool } from '@/tools/calendly/list_event_types'
import { listScheduledEventsTool } from '@/tools/calendly/list_scheduled_events'
import { listWebhooksTool } from '@/tools/calendly/list_webhooks'
import { toResourceUri, toStringArray, toUuid } from '@/tools/calendly/utils'
function buildUrl(tool: { request: { url: unknown } }, params: Record<string, unknown>): string {
const url = tool.request.url
return typeof url === 'function' ? url(params) : (url as string)
}
function buildBody(tool: { request: { body?: unknown } }, params: Record<string, unknown>) {
const body = tool.request.body as (p: Record<string, unknown>) => Record<string, unknown>
return body(params)
}
describe('calendly resource identifiers', () => {
it('extracts the trailing uuid from a nested invitee uri', () => {
expect(toUuid('https://api.calendly.com/scheduled_events/event-1/invitees/invitee-2')).toBe(
'invitee-2'
)
})
it('leaves a bare uuid untouched and strips surrounding whitespace', () => {
expect(toUuid(' event-1 ')).toBe('event-1')
})
it('expands a bare uuid into a canonical uri and passes a full uri through', () => {
expect(toResourceUri(' user-1 ', 'users')).toBe('https://api.calendly.com/users/user-1')
expect(toResourceUri('https://api.calendly.com/users/user-1', 'users')).toBe(
'https://api.calendly.com/users/user-1'
)
})
it('accepts json array params as both an array and a json string', () => {
expect(toStringArray(['a@example.com'], 'eventGuests')).toEqual(['a@example.com'])
expect(toStringArray('["a@example.com"]', 'eventGuests')).toEqual(['a@example.com'])
expect(toStringArray(undefined, 'eventGuests')).toEqual([])
})
it('rejects a json array param that is not an array', () => {
expect(() => toStringArray('{"a":1}', 'eventGuests')).toThrow(/array of strings/)
expect(() => toStringArray('not json', 'eventGuests')).toThrow(/Invalid JSON input/)
})
})
describe('calendly list tools', () => {
it('always sends the scope required by the webhook subscriptions endpoint', () => {
const url = buildUrl(listWebhooksTool, {
apiKey: 'k',
organization: 'org-1',
scope: 'organization',
})
expect(url).toContain('scope=organization')
expect(url).toContain(
`organization=${encodeURIComponent('https://api.calendly.com/organizations/org-1')}`
)
})
it('sends active=false so inactive event types can be listed', () => {
expect(buildUrl(listEventTypesTool, { apiKey: 'k', user: 'user-1', active: false })).toContain(
'active=false'
)
expect(buildUrl(listEventTypesTool, { apiKey: 'k', user: 'user-1', active: true })).toContain(
'active=true'
)
})
it('omits the active filter entirely when it is not set', () => {
expect(buildUrl(listEventTypesTool, { apiKey: 'k', user: 'user-1' })).not.toContain('active=')
})
it('normalizes bare uuids for the shared user and organization filters', () => {
const url = buildUrl(listScheduledEventsTool, { apiKey: 'k', user: 'user-1' })
expect(url).toContain(`user=${encodeURIComponent('https://api.calendly.com/users/user-1')}`)
})
it('still requires a user or organization scope', () => {
expect(() => buildUrl(listScheduledEventsTool, { apiKey: 'k' })).toThrow(
/"user" or "organization"/
)
})
})
describe('calendly booking', () => {
const bookingParams = {
apiKey: 'k',
eventTypeUri: 'event-type-1',
startTime: '2024-01-15T09:00:00Z',
inviteeEmail: 'invitee@example.com',
inviteeTimezone: 'America/New_York',
inviteeName: 'Jane Doe',
}
it('builds the documented booking body', () => {
expect(buildBody(createEventInviteeTool, bookingParams)).toEqual({
event_type: 'https://api.calendly.com/event_types/event-type-1',
start_time: '2024-01-15T09:00:00Z',
invitee: {
email: 'invitee@example.com',
timezone: 'America/New_York',
name: 'Jane Doe',
},
})
})
it('accepts event guests supplied as a json string', () => {
const body = buildBody(createEventInviteeTool, {
...bookingParams,
eventGuests: '["guest@example.com"]',
})
expect(body.event_guests).toEqual(['guest@example.com'])
})
it('rejects a booking with neither a full name nor a first name', () => {
const { inviteeName, ...withoutName } = bookingParams
expect(() => buildBody(createEventInviteeTool, withoutName)).toThrow(
/"inviteeName" or "inviteeFirstName"/
)
})
it('trims the invitee uri when marking a no-show', () => {
expect(
buildBody(createInviteeNoShowTool, {
apiKey: 'k',
inviteeUri: ' https://api.calendly.com/scheduled_events/e-1/invitees/i-1 ',
})
).toEqual({ invitee: 'https://api.calendly.com/scheduled_events/e-1/invitees/i-1' })
})
it('addresses a single invitee by its event and invitee uuids', () => {
expect(
buildUrl(getEventInviteeTool, {
apiKey: 'k',
eventUuid: 'https://api.calendly.com/scheduled_events/e-1',
inviteeUuid: 'i-1',
})
).toBe('https://api.calendly.com/scheduled_events/e-1/invitees/i-1')
})
})
describe('calendly webhook subscriptions', () => {
it('normalizes the organization and user identifiers when creating a webhook', () => {
expect(
buildBody(createWebhookTool, {
apiKey: 'k',
url: 'https://example.com/hook',
events: ['invitee.created'],
organization: 'org-1',
scope: 'user',
user: 'user-1',
})
).toEqual({
url: 'https://example.com/hook',
events: ['invitee.created'],
organization: 'https://api.calendly.com/organizations/org-1',
scope: 'user',
user: 'https://api.calendly.com/users/user-1',
})
})
it('accepts webhook events supplied as a json string', () => {
const body = buildBody(createWebhookTool, {
apiKey: 'k',
url: 'https://example.com/hook',
events: '["invitee.created","invitee.canceled"]',
organization: 'https://api.calendly.com/organizations/org-1',
scope: 'organization',
})
expect(body.events).toEqual(['invitee.created', 'invitee.canceled'])
})
})
+2 -3
View File
@@ -1,4 +1,5 @@
import type { CalendlyCancelEventParams, CalendlyCancelEventResponse } from '@/tools/calendly/types'
import { toUuid } from '@/tools/calendly/utils'
import type { ToolConfig } from '@/tools/types'
export const cancelEventTool: ToolConfig<CalendlyCancelEventParams, CalendlyCancelEventResponse> = {
@@ -31,9 +32,7 @@ export const cancelEventTool: ToolConfig<CalendlyCancelEventParams, CalendlyCanc
request: {
url: (params: CalendlyCancelEventParams) => {
const uuid = params.eventUuid.includes('/')
? params.eventUuid.split('/').pop()
: params.eventUuid
const uuid = toUuid(params.eventUuid)
return `https://api.calendly.com/scheduled_events/${uuid}/cancellation`
},
method: 'POST',
@@ -0,0 +1,163 @@
import type {
CalendlyCreateEventInviteeParams,
CalendlyCreateEventInviteeResponse,
} from '@/tools/calendly/types'
import { toResourceUri, toStringArray } from '@/tools/calendly/utils'
import type { ToolConfig } from '@/tools/types'
export const createEventInviteeTool: ToolConfig<
CalendlyCreateEventInviteeParams,
CalendlyCreateEventInviteeResponse
> = {
id: 'calendly_create_event_invitee',
name: 'Calendly Book Meeting',
description:
'Book a meeting by creating an invitee on an event type at a chosen time. Requires a paid Calendly plan',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Calendly Personal Access Token',
},
eventTypeUri: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Event type to book. Format: UUID (e.g., "abc123-def456") or full URI (e.g., "https://api.calendly.com/event_types/abc123-def456")',
},
startTime: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Start time of the booking in UTC. Must be an available slot. Format: ISO 8601 (e.g., "2024-01-15T09:00:00Z")',
},
inviteeEmail: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Email address of the invitee being booked',
},
inviteeTimezone: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Invitee timezone. Format: IANA timezone (e.g., "America/New_York")',
},
inviteeName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Full name of the invitee. Required when a first name is not provided',
},
inviteeFirstName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'First name of the invitee. Required when a full name is not provided',
},
inviteeLastName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Last name of the invitee',
},
textReminderNumber: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Phone number for SMS reminders. Format: E.164 phone number (e.g., "+14155551234")',
},
eventGuests: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description:
'Additional guests to copy on the invite. Format: array of email strings (max 10, e.g., ["guest@example.com"])',
},
},
request: {
url: () => 'https://api.calendly.com/invitees',
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params: CalendlyCreateEventInviteeParams) => {
if (!params.inviteeName && !params.inviteeFirstName) {
throw new Error(
'Either "inviteeName" or "inviteeFirstName" is required to book a meeting. Please provide the invitee\'s name.'
)
}
const eventGuests = toStringArray(params.eventGuests, 'eventGuests')
return {
event_type: toResourceUri(params.eventTypeUri, 'event_types'),
start_time: params.startTime,
invitee: {
email: params.inviteeEmail,
timezone: params.inviteeTimezone,
...(params.inviteeName && { name: params.inviteeName }),
...(params.inviteeFirstName && { first_name: params.inviteeFirstName }),
...(params.inviteeLastName && { last_name: params.inviteeLastName }),
...(params.textReminderNumber && { text_reminder_number: params.textReminderNumber }),
},
...(eventGuests.length > 0 && { event_guests: eventGuests }),
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: data,
}
},
outputs: {
resource: {
type: 'object',
description: 'The invitee created for the booking',
properties: {
uri: { type: 'string', description: 'Canonical reference to the invitee' },
email: { type: 'string', description: 'Invitee email address' },
name: { type: 'string', description: 'Invitee full name' },
first_name: { type: 'string', description: 'Invitee first name' },
last_name: { type: 'string', description: 'Invitee last name' },
status: { type: 'string', description: 'Invitee status (active or canceled)' },
timezone: { type: 'string', description: 'Invitee timezone' },
event: { type: 'string', description: 'URI of the scheduled event that was booked' },
created_at: { type: 'string', description: 'ISO timestamp when the booking was created' },
updated_at: { type: 'string', description: 'ISO timestamp when the booking was updated' },
cancel_url: { type: 'string', description: 'URL to cancel the booking' },
reschedule_url: { type: 'string', description: 'URL to reschedule the booking' },
rescheduled: { type: 'boolean', description: 'Whether the invitee rescheduled' },
text_reminder_number: {
type: 'string',
description: 'Phone number used for SMS reminders',
},
questions_and_answers: {
type: 'array',
description: 'Responses to custom questions',
items: {
type: 'object',
properties: {
question: { type: 'string', description: 'Question text' },
answer: { type: 'string', description: 'Invitee answer' },
position: { type: 'number', description: 'Question order' },
},
},
},
},
},
},
}
@@ -0,0 +1,64 @@
import type {
CalendlyCreateInviteeNoShowParams,
CalendlyCreateInviteeNoShowResponse,
} from '@/tools/calendly/types'
import type { ToolConfig } from '@/tools/types'
export const createInviteeNoShowTool: ToolConfig<
CalendlyCreateInviteeNoShowParams,
CalendlyCreateInviteeNoShowResponse
> = {
id: 'calendly_create_invitee_no_show',
name: 'Calendly Mark Invitee No-Show',
description: 'Mark an invitee of a scheduled event as a no-show',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Calendly Personal Access Token',
},
inviteeUri: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Full invitee URI to mark as a no-show. Format: URI (e.g., "https://api.calendly.com/scheduled_events/abc123/invitees/def456")',
},
},
request: {
url: () => 'https://api.calendly.com/invitee_no_shows',
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params: CalendlyCreateInviteeNoShowParams) => ({
invitee: params.inviteeUri.trim(),
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: data,
}
},
outputs: {
resource: {
type: 'object',
description: 'The created no-show record',
properties: {
uri: { type: 'string', description: 'Canonical reference to the no-show' },
invitee: { type: 'string', description: 'URI of the invitee marked as a no-show' },
created_at: { type: 'string', description: 'ISO timestamp when the no-show was recorded' },
},
},
},
}
@@ -0,0 +1,70 @@
import type {
CalendlyCreateSchedulingLinkParams,
CalendlyCreateSchedulingLinkResponse,
} from '@/tools/calendly/types'
import { toResourceUri } from '@/tools/calendly/utils'
import type { ToolConfig } from '@/tools/types'
/** Calendly only supports single-use links today, so the event count is fixed at one. */
const MAX_EVENT_COUNT = 1
export const createSchedulingLinkTool: ToolConfig<
CalendlyCreateSchedulingLinkParams,
CalendlyCreateSchedulingLinkResponse
> = {
id: 'calendly_create_scheduling_link',
name: 'Calendly Create Scheduling Link',
description: 'Create a single-use scheduling link for an event type',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Calendly Personal Access Token',
},
eventTypeUri: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Event type the link books. Format: UUID (e.g., "abc123-def456") or full URI (e.g., "https://api.calendly.com/event_types/abc123-def456")',
},
},
request: {
url: () => 'https://api.calendly.com/scheduling_links',
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params: CalendlyCreateSchedulingLinkParams) => ({
max_event_count: MAX_EVENT_COUNT,
owner: toResourceUri(params.eventTypeUri, 'event_types'),
owner_type: 'EventType',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: data,
}
},
outputs: {
resource: {
type: 'object',
description: 'The created scheduling link',
properties: {
booking_url: { type: 'string', description: 'Single-use URL to share with an invitee' },
owner: { type: 'string', description: 'URI of the event type that owns the link' },
owner_type: { type: 'string', description: 'Resource type of the owner' },
},
},
},
}
+4 -3
View File
@@ -2,6 +2,7 @@ import type {
CalendlyCreateWebhookParams,
CalendlyCreateWebhookResponse,
} from '@/tools/calendly/types'
import { toResourceUri, toStringArray } from '@/tools/calendly/utils'
import type { ToolConfig } from '@/tools/types'
export const createWebhookTool: ToolConfig<
@@ -71,13 +72,13 @@ export const createWebhookTool: ToolConfig<
body: (params: CalendlyCreateWebhookParams) => {
const body: any = {
url: params.url,
events: params.events,
organization: params.organization,
events: toStringArray(params.events, 'events'),
organization: toResourceUri(params.organization, 'organizations'),
scope: params.scope,
}
if (params.user && params.scope === 'user') {
body.user = params.user
body.user = toResourceUri(params.user, 'users')
}
if (params.signing_key) {
@@ -0,0 +1,74 @@
import type {
CalendlyDeleteInviteeNoShowParams,
CalendlyDeleteInviteeNoShowResponse,
} from '@/tools/calendly/types'
import { toUuid } from '@/tools/calendly/utils'
import type { ToolConfig } from '@/tools/types'
export const deleteInviteeNoShowTool: ToolConfig<
CalendlyDeleteInviteeNoShowParams,
CalendlyDeleteInviteeNoShowResponse
> = {
id: 'calendly_delete_invitee_no_show',
name: 'Calendly Unmark Invitee No-Show',
description: 'Remove the no-show status from an invitee',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Calendly Personal Access Token',
},
noShowUuid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'No-show UUID to remove. Format: UUID (e.g., "abc123-def456") or full URI (e.g., "https://api.calendly.com/invitee_no_shows/abc123-def456")',
},
},
request: {
url: (params: CalendlyDeleteInviteeNoShowParams) =>
`https://api.calendly.com/invitee_no_shows/${toUuid(params.noShowUuid)}`,
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
if (response.status === 204 || response.status === 200) {
return {
success: true,
output: {
deleted: true,
message: 'No-show status removed successfully',
},
}
}
const data = await response.json()
return {
success: false,
output: {
deleted: false,
message: data.message || 'Failed to remove no-show status',
},
}
},
outputs: {
deleted: {
type: 'boolean',
description: 'Whether the no-show status was successfully removed',
},
message: {
type: 'string',
description: 'Status message',
},
},
}
+2 -3
View File
@@ -2,6 +2,7 @@ import type {
CalendlyDeleteWebhookParams,
CalendlyDeleteWebhookResponse,
} from '@/tools/calendly/types'
import { toUuid } from '@/tools/calendly/utils'
import type { ToolConfig } from '@/tools/types'
export const deleteWebhookTool: ToolConfig<
@@ -31,9 +32,7 @@ export const deleteWebhookTool: ToolConfig<
request: {
url: (params: CalendlyDeleteWebhookParams) => {
const uuid = params.webhookUuid.includes('/')
? params.webhookUuid.split('/').pop()
: params.webhookUuid
const uuid = toUuid(params.webhookUuid)
return `https://api.calendly.com/webhook_subscriptions/${uuid}`
},
method: 'DELETE',
@@ -0,0 +1,145 @@
import type {
CalendlyGetEventInviteeParams,
CalendlyGetEventInviteeResponse,
} from '@/tools/calendly/types'
import { toUuid } from '@/tools/calendly/utils'
import type { ToolConfig } from '@/tools/types'
export const getEventInviteeTool: ToolConfig<
CalendlyGetEventInviteeParams,
CalendlyGetEventInviteeResponse
> = {
id: 'calendly_get_event_invitee',
name: 'Calendly Get Event Invitee',
description: 'Retrieve a single invitee of a scheduled event, including their intake answers',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Calendly Personal Access Token',
},
eventUuid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Scheduled event UUID. Format: UUID (e.g., "abc123-def456") or full URI (e.g., "https://api.calendly.com/scheduled_events/abc123-def456")',
},
inviteeUuid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Invitee UUID. Format: UUID (e.g., "abc123-def456") or full invitee URI (e.g., "https://api.calendly.com/scheduled_events/abc123/invitees/def456")',
},
},
request: {
url: (params: CalendlyGetEventInviteeParams) =>
`https://api.calendly.com/scheduled_events/${toUuid(params.eventUuid)}/invitees/${toUuid(params.inviteeUuid)}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: data,
}
},
outputs: {
resource: {
type: 'object',
description: 'Invitee details',
properties: {
uri: { type: 'string', description: 'Canonical reference to the invitee' },
email: { type: 'string', description: 'Invitee email address' },
name: { type: 'string', description: 'Invitee full name' },
first_name: { type: 'string', description: 'Invitee first name' },
last_name: { type: 'string', description: 'Invitee last name' },
status: { type: 'string', description: 'Invitee status (active or canceled)' },
timezone: { type: 'string', description: 'Invitee timezone' },
event: { type: 'string', description: 'URI of the scheduled event' },
created_at: { type: 'string', description: 'ISO timestamp when invitee was created' },
updated_at: { type: 'string', description: 'ISO timestamp when invitee was updated' },
cancel_url: { type: 'string', description: 'URL to cancel the booking' },
reschedule_url: { type: 'string', description: 'URL to reschedule the booking' },
rescheduled: { type: 'boolean', description: 'Whether the invitee rescheduled' },
text_reminder_number: {
type: 'string',
description: 'Phone number used for SMS reminders',
},
routing_form_submission: {
type: 'string',
description: 'URI of the routing form submission that produced this booking',
},
questions_and_answers: {
type: 'array',
description: 'Responses to custom questions',
items: {
type: 'object',
properties: {
question: { type: 'string', description: 'Question text' },
answer: { type: 'string', description: 'Invitee answer' },
position: { type: 'number', description: 'Question order' },
},
},
},
tracking: {
type: 'object',
description: 'UTM and Salesforce tracking parameters captured at booking',
properties: {
utm_campaign: { type: 'string', description: 'UTM campaign' },
utm_source: { type: 'string', description: 'UTM source' },
utm_medium: { type: 'string', description: 'UTM medium' },
utm_content: { type: 'string', description: 'UTM content' },
utm_term: { type: 'string', description: 'UTM term' },
salesforce_uuid: { type: 'string', description: 'Salesforce record identifier' },
},
},
cancellation: {
type: 'object',
description: 'Cancellation details when the invitee has canceled',
optional: true,
properties: {
canceled_by: { type: 'string', description: 'Name of person who canceled' },
reason: { type: 'string', description: 'Cancellation reason' },
canceler_type: { type: 'string', description: 'Type of canceler (host or invitee)' },
created_at: { type: 'string', description: 'ISO timestamp of the cancellation' },
},
},
no_show: {
type: 'object',
description: 'No-show record when the invitee has been marked as a no-show',
optional: true,
properties: {
uri: { type: 'string', description: 'Canonical reference to the no-show' },
created_at: { type: 'string', description: 'ISO timestamp when marked as no-show' },
},
},
payment: {
type: 'object',
description: 'Payment collected at booking',
optional: true,
properties: {
external_id: { type: 'string', description: 'Payment identifier at the provider' },
provider: { type: 'string', description: 'Payment provider' },
amount: { type: 'number', description: 'Amount charged' },
currency: { type: 'string', description: 'Currency code' },
terms: { type: 'string', description: 'Payment terms' },
successful: { type: 'boolean', description: 'Whether the payment succeeded' },
},
},
},
},
},
}
+2 -3
View File
@@ -2,6 +2,7 @@ import type {
CalendlyGetEventTypeParams,
CalendlyGetEventTypeResponse,
} from '@/tools/calendly/types'
import { toUuid } from '@/tools/calendly/utils'
import type { ToolConfig } from '@/tools/types'
export const getEventTypeTool: ToolConfig<
@@ -31,9 +32,7 @@ export const getEventTypeTool: ToolConfig<
request: {
url: (params: CalendlyGetEventTypeParams) => {
const uuid = params.eventTypeUuid.includes('/')
? params.eventTypeUuid.split('/').pop()
: params.eventTypeUuid
const uuid = toUuid(params.eventTypeUuid)
return `https://api.calendly.com/event_types/${uuid}`
},
method: 'GET',
@@ -2,6 +2,7 @@ import type {
CalendlyGetScheduledEventParams,
CalendlyGetScheduledEventResponse,
} from '@/tools/calendly/types'
import { toUuid } from '@/tools/calendly/utils'
import type { ToolConfig } from '@/tools/types'
export const getScheduledEventTool: ToolConfig<
@@ -31,9 +32,7 @@ export const getScheduledEventTool: ToolConfig<
request: {
url: (params: CalendlyGetScheduledEventParams) => {
const uuid = params.eventUuid.includes('/')
? params.eventUuid.split('/').pop()
: params.eventUuid
const uuid = toUuid(params.eventUuid)
return `https://api.calendly.com/scheduled_events/${uuid}`
},
method: 'GET',
+67
View File
@@ -0,0 +1,67 @@
import type { CalendlyGetUserParams, CalendlyGetUserResponse } from '@/tools/calendly/types'
import { toUuid } from '@/tools/calendly/utils'
import type { ToolConfig } from '@/tools/types'
export const getUserTool: ToolConfig<CalendlyGetUserParams, CalendlyGetUserResponse> = {
id: 'calendly_get_user',
name: 'Calendly Get User',
description: 'Get information about a specific Calendly user',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Calendly Personal Access Token',
},
userUuid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'User UUID. Format: UUID (e.g., "abc123-def456"), full URI (e.g., "https://api.calendly.com/users/abc123-def456"), or the constant "me" for the authenticated user',
},
},
request: {
url: (params: CalendlyGetUserParams) =>
`https://api.calendly.com/users/${toUuid(params.userUuid)}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: data,
}
},
outputs: {
resource: {
type: 'object',
description: 'User information',
properties: {
uri: { type: 'string', description: 'Canonical reference to the user' },
name: { type: 'string', description: 'User full name' },
slug: { type: 'string', description: 'Unique identifier for the user in URLs' },
email: { type: 'string', description: 'User email address' },
scheduling_url: { type: 'string', description: "URL to the user's scheduling page" },
timezone: { type: 'string', description: 'User timezone' },
time_notation: { type: 'string', description: 'Time notation preference (12h or 24h)' },
avatar_url: { type: 'string', description: 'URL to user avatar image' },
created_at: { type: 'string', description: 'ISO timestamp when user was created' },
updated_at: { type: 'string', description: 'ISO timestamp when user was last updated' },
current_organization: { type: 'string', description: 'URI of current organization' },
resource_type: { type: 'string', description: 'Resource type' },
locale: { type: 'string', description: 'User locale' },
},
},
},
}
+24
View File
@@ -1,21 +1,45 @@
import { cancelEventTool } from '@/tools/calendly/cancel_event'
import { createEventInviteeTool } from '@/tools/calendly/create_event_invitee'
import { createInviteeNoShowTool } from '@/tools/calendly/create_invitee_no_show'
import { createSchedulingLinkTool } from '@/tools/calendly/create_scheduling_link'
import { createWebhookTool } from '@/tools/calendly/create_webhook'
import { deleteInviteeNoShowTool } from '@/tools/calendly/delete_invitee_no_show'
import { deleteWebhookTool } from '@/tools/calendly/delete_webhook'
import { getCurrentUserTool } from '@/tools/calendly/get_current_user'
import { getEventInviteeTool } from '@/tools/calendly/get_event_invitee'
import { getEventTypeTool } from '@/tools/calendly/get_event_type'
import { getScheduledEventTool } from '@/tools/calendly/get_scheduled_event'
import { getUserTool } from '@/tools/calendly/get_user'
import { listEventInviteesTool } from '@/tools/calendly/list_event_invitees'
import { listEventTypeAvailableTimesTool } from '@/tools/calendly/list_event_type_available_times'
import { listEventTypesTool } from '@/tools/calendly/list_event_types'
import { listOrganizationMembershipsTool } from '@/tools/calendly/list_organization_memberships'
import { listRoutingFormSubmissionsTool } from '@/tools/calendly/list_routing_form_submissions'
import { listRoutingFormsTool } from '@/tools/calendly/list_routing_forms'
import { listScheduledEventsTool } from '@/tools/calendly/list_scheduled_events'
import { listUserAvailabilitySchedulesTool } from '@/tools/calendly/list_user_availability_schedules'
import { listUserBusyTimesTool } from '@/tools/calendly/list_user_busy_times'
import { listWebhooksTool } from '@/tools/calendly/list_webhooks'
export const calendlyGetCurrentUserTool = getCurrentUserTool
export const calendlyGetUserTool = getUserTool
export const calendlyListEventTypesTool = listEventTypesTool
export const calendlyGetEventTypeTool = getEventTypeTool
export const calendlyListEventTypeAvailableTimesTool = listEventTypeAvailableTimesTool
export const calendlyListScheduledEventsTool = listScheduledEventsTool
export const calendlyGetScheduledEventTool = getScheduledEventTool
export const calendlyListEventInviteesTool = listEventInviteesTool
export const calendlyGetEventInviteeTool = getEventInviteeTool
export const calendlyCreateEventInviteeTool = createEventInviteeTool
export const calendlyCancelEventTool = cancelEventTool
export const calendlyCreateInviteeNoShowTool = createInviteeNoShowTool
export const calendlyDeleteInviteeNoShowTool = deleteInviteeNoShowTool
export const calendlyCreateSchedulingLinkTool = createSchedulingLinkTool
export const calendlyListUserBusyTimesTool = listUserBusyTimesTool
export const calendlyListUserAvailabilitySchedulesTool = listUserAvailabilitySchedulesTool
export const calendlyListOrganizationMembershipsTool = listOrganizationMembershipsTool
export const calendlyListRoutingFormsTool = listRoutingFormsTool
export const calendlyListRoutingFormSubmissionsTool = listRoutingFormSubmissionsTool
export const calendlyListWebhooksTool = listWebhooksTool
export const calendlyCreateWebhookTool = createWebhookTool
export const calendlyDeleteWebhookTool = deleteWebhookTool
@@ -2,6 +2,7 @@ import type {
CalendlyListEventInviteesParams,
CalendlyListEventInviteesResponse,
} from '@/tools/calendly/types'
import { toUuid } from '@/tools/calendly/utils'
import type { ToolConfig } from '@/tools/types'
export const listEventInviteesTool: ToolConfig<
@@ -63,9 +64,7 @@ export const listEventInviteesTool: ToolConfig<
request: {
url: (params: CalendlyListEventInviteesParams) => {
const uuid = params.eventUuid.includes('/')
? params.eventUuid.split('/').pop()
: params.eventUuid
const uuid = toUuid(params.eventUuid)
const url = `https://api.calendly.com/scheduled_events/${uuid}/invitees`
const queryParams = []
@@ -0,0 +1,91 @@
import type {
CalendlyListEventTypeAvailableTimesParams,
CalendlyListEventTypeAvailableTimesResponse,
} from '@/tools/calendly/types'
import { toResourceUri } from '@/tools/calendly/utils'
import type { ToolConfig } from '@/tools/types'
export const listEventTypeAvailableTimesTool: ToolConfig<
CalendlyListEventTypeAvailableTimesParams,
CalendlyListEventTypeAvailableTimesResponse
> = {
id: 'calendly_list_event_type_available_times',
name: 'Calendly List Event Type Available Times',
description: 'Retrieve bookable time slots for an event type within a date range',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Calendly Personal Access Token',
},
eventTypeUri: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Event type to check. Format: UUID (e.g., "abc123-def456") or full URI (e.g., "https://api.calendly.com/event_types/abc123-def456")',
},
startTime: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Start of the availability range. Cannot be in the past. Format: ISO 8601 (e.g., "2024-01-01T00:00:00Z")',
},
endTime: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'End of the availability range. Must be in the future and no more than 31 days after the start. Format: ISO 8601 (e.g., "2024-01-15T00:00:00Z")',
},
},
request: {
url: (params: CalendlyListEventTypeAvailableTimesParams) => {
const queryParams = [
`event_type=${encodeURIComponent(toResourceUri(params.eventTypeUri, 'event_types'))}`,
`start_time=${encodeURIComponent(params.startTime)}`,
`end_time=${encodeURIComponent(params.endTime)}`,
]
return `https://api.calendly.com/event_type_available_times?${queryParams.join('&')}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: data,
}
},
outputs: {
collection: {
type: 'array',
description: 'Array of available time slots',
items: {
type: 'object',
properties: {
status: { type: 'string', description: 'Availability status of the slot' },
invitees_remaining: {
type: 'number',
description: 'Number of invitees that can still book this slot',
},
start_time: { type: 'string', description: 'ISO timestamp of the slot start' },
scheduling_url: { type: 'string', description: 'URL that books this exact slot' },
},
},
},
},
}
+7 -4
View File
@@ -2,6 +2,7 @@ import type {
CalendlyListEventTypesParams,
CalendlyListEventTypesResponse,
} from '@/tools/calendly/types'
import { toResourceUri } from '@/tools/calendly/utils'
import type { ToolConfig } from '@/tools/types'
export const listEventTypesTool: ToolConfig<
@@ -69,11 +70,13 @@ export const listEventTypesTool: ToolConfig<
const queryParams = []
if (params.user) {
queryParams.push(`user=${encodeURIComponent(params.user)}`)
queryParams.push(`user=${encodeURIComponent(toResourceUri(params.user, 'users'))}`)
}
if (params.organization) {
queryParams.push(`organization=${encodeURIComponent(params.organization)}`)
queryParams.push(
`organization=${encodeURIComponent(toResourceUri(params.organization, 'organizations'))}`
)
}
if (params.count) {
@@ -88,8 +91,8 @@ export const listEventTypesTool: ToolConfig<
queryParams.push(`sort=${encodeURIComponent(params.sort)}`)
}
if (params.active === true) {
queryParams.push('active=true')
if (params.active !== undefined) {
queryParams.push(`active=${params.active}`)
}
return queryParams.length > 0 ? `${url}?${queryParams.join('&')}` : url
@@ -0,0 +1,167 @@
import type {
CalendlyListOrganizationMembershipsParams,
CalendlyListOrganizationMembershipsResponse,
} from '@/tools/calendly/types'
import { toResourceUri } from '@/tools/calendly/utils'
import type { ToolConfig } from '@/tools/types'
export const listOrganizationMembershipsTool: ToolConfig<
CalendlyListOrganizationMembershipsParams,
CalendlyListOrganizationMembershipsResponse
> = {
id: 'calendly_list_organization_memberships',
name: 'Calendly List Organization Memberships',
description: 'Retrieve the members of an organization, including each member profile and role',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Calendly Personal Access Token',
},
organization: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Return memberships that belong to this organization. Format: UUID (e.g., "abc123-def456") or full URI (e.g., "https://api.calendly.com/organizations/abc123-def456")',
},
user: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Return memberships that belong to this user. Format: UUID (e.g., "abc123-def456") or full URI (e.g., "https://api.calendly.com/users/abc123-def456")',
},
email: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Filter memberships by member email address',
},
role: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by role. Format: "owner", "admin", or "user"',
},
count: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results per page. Format: integer (default: 20, max: 100)',
},
pageToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Page token for pagination. Format: opaque string from previous response next_page_token',
},
},
request: {
url: (params: CalendlyListOrganizationMembershipsParams) => {
const url = 'https://api.calendly.com/organization_memberships'
const queryParams = []
if (!params.user && !params.organization) {
throw new Error(
'At least one of "user" or "organization" parameter is required. Please provide either a user URI or organization URI.'
)
}
if (params.organization) {
queryParams.push(
`organization=${encodeURIComponent(toResourceUri(params.organization, 'organizations'))}`
)
}
if (params.user) {
queryParams.push(`user=${encodeURIComponent(toResourceUri(params.user, 'users'))}`)
}
if (params.email) {
queryParams.push(`email=${encodeURIComponent(params.email)}`)
}
if (params.role) {
queryParams.push(`role=${encodeURIComponent(params.role)}`)
}
if (params.count) {
queryParams.push(`count=${Number(params.count)}`)
}
if (params.pageToken) {
queryParams.push(`page_token=${encodeURIComponent(params.pageToken)}`)
}
return `${url}?${queryParams.join('&')}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: data,
}
},
outputs: {
collection: {
type: 'array',
description: 'Array of organization membership objects',
items: {
type: 'object',
properties: {
uri: { type: 'string', description: 'Canonical reference to the membership' },
role: { type: 'string', description: 'Member role (owner, admin, or user)' },
organization: { type: 'string', description: 'URI of the organization' },
created_at: { type: 'string', description: 'ISO timestamp when the member joined' },
updated_at: { type: 'string', description: 'ISO timestamp when the membership changed' },
user: {
type: 'object',
description: 'The member',
properties: {
uri: { type: 'string', description: 'Canonical reference to the user' },
name: { type: 'string', description: 'User full name' },
slug: { type: 'string', description: 'Unique identifier for the user in URLs' },
email: { type: 'string', description: 'User email address' },
scheduling_url: { type: 'string', description: "URL to the user's scheduling page" },
timezone: { type: 'string', description: 'User timezone' },
time_notation: {
type: 'string',
description: 'Time notation preference (12h or 24h)',
},
avatar_url: { type: 'string', description: 'URL to user avatar image' },
locale: { type: 'string', description: 'User locale' },
created_at: { type: 'string', description: 'ISO timestamp when user was created' },
updated_at: { type: 'string', description: 'ISO timestamp when user was updated' },
},
},
},
},
},
pagination: {
type: 'object',
description: 'Pagination information',
properties: {
count: { type: 'number', description: 'Number of results in this page' },
next_page: { type: 'string', description: 'URL to next page (if available)' },
previous_page: { type: 'string', description: 'URL to previous page (if available)' },
next_page_token: { type: 'string', description: 'Token for next page' },
previous_page_token: { type: 'string', description: 'Token for previous page' },
},
},
},
}
@@ -0,0 +1,155 @@
import type {
CalendlyListRoutingFormSubmissionsParams,
CalendlyListRoutingFormSubmissionsResponse,
} from '@/tools/calendly/types'
import { toResourceUri } from '@/tools/calendly/utils'
import type { ToolConfig } from '@/tools/types'
export const listRoutingFormSubmissionsTool: ToolConfig<
CalendlyListRoutingFormSubmissionsParams,
CalendlyListRoutingFormSubmissionsResponse
> = {
id: 'calendly_list_routing_form_submissions',
name: 'Calendly List Routing Form Submissions',
description: 'Retrieve the submissions of a routing form, including answers and routing result',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Calendly Personal Access Token',
},
formUri: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Routing form whose submissions are returned. Format: UUID (e.g., "abc123-def456") or full URI (e.g., "https://api.calendly.com/routing_forms/abc123-def456")',
},
count: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results per page. Format: integer (default: 20, max: 100)',
},
pageToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Page token for pagination. Format: opaque string from previous response next_page_token',
},
sort: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Sort order for results. Format: "created_at:direction" (e.g., "created_at:asc", "created_at:desc")',
},
},
request: {
url: (params: CalendlyListRoutingFormSubmissionsParams) => {
const queryParams = [
`form=${encodeURIComponent(toResourceUri(params.formUri, 'routing_forms'))}`,
]
if (params.count) {
queryParams.push(`count=${Number(params.count)}`)
}
if (params.pageToken) {
queryParams.push(`page_token=${encodeURIComponent(params.pageToken)}`)
}
if (params.sort) {
queryParams.push(`sort=${encodeURIComponent(params.sort)}`)
}
return `https://api.calendly.com/routing_form_submissions?${queryParams.join('&')}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: data,
}
},
outputs: {
collection: {
type: 'array',
description: 'Array of routing form submission objects',
items: {
type: 'object',
properties: {
uri: { type: 'string', description: 'Canonical reference to the submission' },
routing_form: { type: 'string', description: 'URI of the routing form' },
submitter: {
type: 'string',
description: 'URI of the invitee who submitted, when the submission led to a booking',
},
submitter_type: { type: 'string', description: 'Type of the submitter' },
created_at: { type: 'string', description: 'ISO timestamp when the form was submitted' },
updated_at: {
type: 'string',
description: 'ISO timestamp when the submission was updated',
},
questions_and_answers: {
type: 'array',
description: 'Answers given on the routing form',
items: {
type: 'object',
properties: {
question_uuid: { type: 'string', description: 'Question identifier' },
question: { type: 'string', description: 'Question text' },
answer: { type: 'string', description: 'Submitted answer' },
},
},
},
tracking: {
type: 'object',
description: 'UTM and Salesforce tracking parameters captured at submission',
properties: {
utm_campaign: { type: 'string', description: 'UTM campaign' },
utm_source: { type: 'string', description: 'UTM source' },
utm_medium: { type: 'string', description: 'UTM medium' },
utm_content: { type: 'string', description: 'UTM content' },
utm_term: { type: 'string', description: 'UTM term' },
salesforce_uuid: { type: 'string', description: 'Salesforce record identifier' },
},
},
result: {
type: 'object',
description: 'Where the submission routed to',
properties: {
type: { type: 'string', description: 'Routing result type' },
value: { type: 'string', description: 'Routing destination' },
},
},
},
},
},
pagination: {
type: 'object',
description: 'Pagination information',
properties: {
count: { type: 'number', description: 'Number of results in this page' },
next_page: { type: 'string', description: 'URL to next page (if available)' },
previous_page: { type: 'string', description: 'URL to previous page (if available)' },
next_page_token: { type: 'string', description: 'Token for next page' },
previous_page_token: { type: 'string', description: 'Token for previous page' },
},
},
},
}
@@ -0,0 +1,135 @@
import type {
CalendlyListRoutingFormsParams,
CalendlyListRoutingFormsResponse,
} from '@/tools/calendly/types'
import { toResourceUri } from '@/tools/calendly/utils'
import type { ToolConfig } from '@/tools/types'
export const listRoutingFormsTool: ToolConfig<
CalendlyListRoutingFormsParams,
CalendlyListRoutingFormsResponse
> = {
id: 'calendly_list_routing_forms',
name: 'Calendly List Routing Forms',
description: 'Retrieve the routing forms of an organization, including their questions',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Calendly Personal Access Token',
},
organization: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Organization whose routing forms are returned. Format: UUID (e.g., "abc123-def456") or full URI (e.g., "https://api.calendly.com/organizations/abc123-def456")',
},
count: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results per page. Format: integer (default: 20, max: 100)',
},
pageToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Page token for pagination. Format: opaque string from previous response next_page_token',
},
sort: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Sort order for results. Format: "created_at:direction" (e.g., "created_at:asc", "created_at:desc")',
},
},
request: {
url: (params: CalendlyListRoutingFormsParams) => {
const queryParams = [
`organization=${encodeURIComponent(toResourceUri(params.organization, 'organizations'))}`,
]
if (params.count) {
queryParams.push(`count=${Number(params.count)}`)
}
if (params.pageToken) {
queryParams.push(`page_token=${encodeURIComponent(params.pageToken)}`)
}
if (params.sort) {
queryParams.push(`sort=${encodeURIComponent(params.sort)}`)
}
return `https://api.calendly.com/routing_forms?${queryParams.join('&')}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: data,
}
},
outputs: {
collection: {
type: 'array',
description: 'Array of routing form objects',
items: {
type: 'object',
properties: {
uri: { type: 'string', description: 'Canonical reference to the routing form' },
organization: { type: 'string', description: 'URI of the owning organization' },
name: { type: 'string', description: 'Routing form name' },
status: { type: 'string', description: 'Routing form status (published or draft)' },
created_at: { type: 'string', description: 'ISO timestamp when the form was created' },
updated_at: { type: 'string', description: 'ISO timestamp when the form was updated' },
questions: {
type: 'array',
description: 'Questions asked by the routing form',
items: {
type: 'object',
properties: {
uuid: { type: 'string', description: 'Question identifier' },
name: { type: 'string', description: 'Question text' },
type: { type: 'string', description: 'Question answer type' },
required: { type: 'boolean', description: 'Whether an answer is required' },
answer_choices: {
type: 'array',
description: 'Selectable answers for choice questions',
items: { type: 'string' },
},
},
},
},
},
},
},
pagination: {
type: 'object',
description: 'Pagination information',
properties: {
count: { type: 'number', description: 'Number of results in this page' },
next_page: { type: 'string', description: 'URL to next page (if available)' },
previous_page: { type: 'string', description: 'URL to previous page (if available)' },
next_page_token: { type: 'string', description: 'Token for next page' },
previous_page_token: { type: 'string', description: 'Token for previous page' },
},
},
},
}
@@ -2,6 +2,7 @@ import type {
CalendlyListScheduledEventsParams,
CalendlyListScheduledEventsResponse,
} from '@/tools/calendly/types'
import { toResourceUri } from '@/tools/calendly/utils'
import type { ToolConfig } from '@/tools/types'
export const listScheduledEventsTool: ToolConfig<
@@ -94,11 +95,13 @@ export const listScheduledEventsTool: ToolConfig<
}
if (params.user) {
queryParams.push(`user=${encodeURIComponent(params.user)}`)
queryParams.push(`user=${encodeURIComponent(toResourceUri(params.user, 'users'))}`)
}
if (params.organization) {
queryParams.push(`organization=${encodeURIComponent(params.organization)}`)
queryParams.push(
`organization=${encodeURIComponent(toResourceUri(params.organization, 'organizations'))}`
)
}
if (params.invitee_email) {
@@ -0,0 +1,97 @@
import type {
CalendlyListUserAvailabilitySchedulesParams,
CalendlyListUserAvailabilitySchedulesResponse,
} from '@/tools/calendly/types'
import { toResourceUri } from '@/tools/calendly/utils'
import type { ToolConfig } from '@/tools/types'
export const listUserAvailabilitySchedulesTool: ToolConfig<
CalendlyListUserAvailabilitySchedulesParams,
CalendlyListUserAvailabilitySchedulesResponse
> = {
id: 'calendly_list_user_availability_schedules',
name: 'Calendly List User Availability Schedules',
description: "Retrieve a user's availability schedules, working hours, and date overrides",
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Calendly Personal Access Token',
},
user: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'User whose schedules are returned. Format: UUID (e.g., "abc123-def456") or full URI (e.g., "https://api.calendly.com/users/abc123-def456")',
},
},
request: {
url: (params: CalendlyListUserAvailabilitySchedulesParams) =>
`https://api.calendly.com/user_availability_schedules?user=${encodeURIComponent(toResourceUri(params.user, 'users'))}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: data,
}
},
outputs: {
collection: {
type: 'array',
description: 'Array of availability schedules',
items: {
type: 'object',
properties: {
uri: { type: 'string', description: 'Canonical reference to the schedule' },
name: { type: 'string', description: 'Schedule name' },
default: { type: 'boolean', description: "Whether this is the user's default schedule" },
user: { type: 'string', description: 'URI of the owning user' },
timezone: { type: 'string', description: 'Timezone the schedule is defined in' },
rules: {
type: 'array',
description: 'Weekly rules and date overrides that make up the schedule',
items: {
type: 'object',
properties: {
type: { type: 'string', description: 'Rule type (wday or date)' },
wday: {
type: 'string',
description: 'Day of week the rule applies to, for wday rules',
},
date: {
type: 'string',
description: 'Calendar date the rule overrides, for date rules',
},
intervals: {
type: 'array',
description: 'Available intervals for the rule; empty means unavailable',
items: {
type: 'object',
properties: {
from: { type: 'string', description: 'Interval start time (HH:MM)' },
to: { type: 'string', description: 'Interval end time (HH:MM)' },
},
},
},
},
},
},
},
},
},
},
}
@@ -0,0 +1,109 @@
import type {
CalendlyListUserBusyTimesParams,
CalendlyListUserBusyTimesResponse,
} from '@/tools/calendly/types'
import { toResourceUri } from '@/tools/calendly/utils'
import type { ToolConfig } from '@/tools/types'
export const listUserBusyTimesTool: ToolConfig<
CalendlyListUserBusyTimesParams,
CalendlyListUserBusyTimesResponse
> = {
id: 'calendly_list_user_busy_times',
name: 'Calendly List User Busy Times',
description:
"Retrieve a user's internal and external busy times within a date range, based on their connected calendars",
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Calendly Personal Access Token',
},
user: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'User whose busy times are returned. Format: UUID (e.g., "abc123-def456") or full URI (e.g., "https://api.calendly.com/users/abc123-def456")',
},
startTime: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Start of the requested range. Cannot be in the past. Format: ISO 8601 (e.g., "2024-01-01T00:00:00Z")',
},
endTime: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'End of the requested range. Must be after the start and no more than 7 days later. Format: ISO 8601 (e.g., "2024-01-07T00:00:00Z")',
},
},
request: {
url: (params: CalendlyListUserBusyTimesParams) => {
const queryParams = [
`user=${encodeURIComponent(toResourceUri(params.user, 'users'))}`,
`start_time=${encodeURIComponent(params.startTime)}`,
`end_time=${encodeURIComponent(params.endTime)}`,
]
return `https://api.calendly.com/user_busy_times?${queryParams.join('&')}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: data,
}
},
outputs: {
collection: {
type: 'array',
description: 'Array of busy time blocks',
items: {
type: 'object',
properties: {
type: {
type: 'string',
description: 'Source of the busy block (calendly, external, or reserved)',
},
start_time: { type: 'string', description: 'ISO timestamp when the block starts' },
end_time: { type: 'string', description: 'ISO timestamp when the block ends' },
buffered_start_time: {
type: 'string',
description: 'ISO timestamp when the block starts including buffer',
optional: true,
},
buffered_end_time: {
type: 'string',
description: 'ISO timestamp when the block ends including buffer',
optional: true,
},
event: {
type: 'object',
description: 'The Calendly event occupying this block',
optional: true,
properties: {
uri: { type: 'string', description: 'URI of the scheduled event' },
},
},
},
},
},
},
}
+8 -7
View File
@@ -2,6 +2,7 @@ import type {
CalendlyListWebhooksParams,
CalendlyListWebhooksResponse,
} from '@/tools/calendly/types'
import { toResourceUri } from '@/tools/calendly/utils'
import type { ToolConfig } from '@/tools/types'
export const listWebhooksTool: ToolConfig<
@@ -42,9 +43,9 @@ export const listWebhooksTool: ToolConfig<
},
scope: {
type: 'string',
required: false,
required: true,
visibility: 'user-or-llm',
description: 'Filter by scope. Format: "organization" or "user"',
description: 'Scope of the webhooks to list. Format: "organization", "user", or "group"',
},
user: {
type: 'string',
@@ -60,7 +61,9 @@ export const listWebhooksTool: ToolConfig<
const url = 'https://api.calendly.com/webhook_subscriptions'
const queryParams = []
queryParams.push(`organization=${encodeURIComponent(params.organization)}`)
queryParams.push(
`organization=${encodeURIComponent(toResourceUri(params.organization, 'organizations'))}`
)
if (params.count) {
queryParams.push(`count=${Number(params.count)}`)
@@ -70,12 +73,10 @@ export const listWebhooksTool: ToolConfig<
queryParams.push(`page_token=${encodeURIComponent(params.pageToken)}`)
}
if (params.scope) {
queryParams.push(`scope=${encodeURIComponent(params.scope)}`)
}
queryParams.push(`scope=${encodeURIComponent(params.scope)}`)
if (params.user) {
queryParams.push(`user=${encodeURIComponent(params.user)}`)
queryParams.push(`user=${encodeURIComponent(toResourceUri(params.user, 'users'))}`)
}
return `${url}?${queryParams.join('&')}`
+341 -16
View File
@@ -1,25 +1,49 @@
import type { ToolResponse } from '@/tools/types'
/** Shared pagination envelope returned by every Calendly list endpoint. */
export interface CalendlyPagination {
count: number
next_page: string | null
previous_page: string | null
next_page_token: string | null
previous_page_token: string | null
}
/** The User resource, returned by both `/users/me` and `/users/{uuid}`. */
export interface CalendlyUserResource {
uri: string
name: string
slug: string
email: string
scheduling_url: string
timezone: string
time_notation: string
avatar_url: string | null
created_at: string
updated_at: string
current_organization: string
resource_type: string
locale: string
}
export interface CalendlyGetCurrentUserParams {
apiKey: string
}
export interface CalendlyGetCurrentUserResponse extends ToolResponse {
output: {
resource: {
uri: string
name: string
slug: string
email: string
scheduling_url: string
timezone: string
avatar_url: string
created_at: string
updated_at: string
current_organization: string
resource_type: string
locale: string
}
resource: CalendlyUserResource
}
}
export interface CalendlyGetUserParams {
apiKey: string
userUuid: string
}
export interface CalendlyGetUserResponse extends ToolResponse {
output: {
resource: CalendlyUserResource
}
}
@@ -311,9 +335,9 @@ export interface CalendlyCancelEventResponse extends ToolResponse {
export interface CalendlyListWebhooksParams {
apiKey: string
organization: string
scope: string
count?: number
pageToken?: string
scope?: string
user?: string
}
@@ -346,7 +370,7 @@ export interface CalendlyListWebhooksResponse extends ToolResponse {
export interface CalendlyCreateWebhookParams {
apiKey: string
url: string
events: string[]
events: string[] | string
organization: string
user?: string
scope: string
@@ -383,3 +407,304 @@ export interface CalendlyDeleteWebhookResponse extends ToolResponse {
message: string
}
}
/** The Invitee resource, returned by the single-invitee read and by the booking endpoint. */
export interface CalendlyInviteeResource {
uri: string
email: string
name: string
first_name: string | null
last_name: string | null
status: string
timezone: string
event: string
created_at: string
updated_at: string
cancel_url: string
reschedule_url: string
rescheduled: boolean
old_invitee: string | null
new_invitee: string | null
text_reminder_number: string | null
routing_form_submission: string | null
scheduling_method: string | null
invitee_scheduled_by: string | null
questions_and_answers: Array<{
question: string
answer: string
position: number
}>
tracking: {
utm_campaign: string | null
utm_source: string | null
utm_medium: string | null
utm_content: string | null
utm_term: string | null
salesforce_uuid: string | null
}
cancellation?: {
canceled_by: string
reason: string
canceler_type: string
created_at: string
}
payment?: {
external_id: string
provider: string
amount: number
currency: string
terms: string
successful: boolean
} | null
no_show?: {
uri: string
created_at: string
} | null
reconfirmation?: {
created_at: string
confirmed_at: string | null
} | null
}
export interface CalendlyGetEventInviteeParams {
apiKey: string
eventUuid: string
inviteeUuid: string
}
export interface CalendlyGetEventInviteeResponse extends ToolResponse {
output: {
resource: CalendlyInviteeResource
}
}
export interface CalendlyCreateEventInviteeParams {
apiKey: string
eventTypeUri: string
startTime: string
inviteeEmail: string
inviteeTimezone: string
inviteeName?: string
inviteeFirstName?: string
inviteeLastName?: string
textReminderNumber?: string
eventGuests?: string[] | string
}
export interface CalendlyCreateEventInviteeResponse extends ToolResponse {
output: {
resource: CalendlyInviteeResource
}
}
export interface CalendlyListEventTypeAvailableTimesParams {
apiKey: string
eventTypeUri: string
startTime: string
endTime: string
}
export interface CalendlyListEventTypeAvailableTimesResponse extends ToolResponse {
output: {
collection: Array<{
status: string
invitees_remaining: number
start_time: string
scheduling_url: string
}>
}
}
export interface CalendlyListUserBusyTimesParams {
apiKey: string
user: string
startTime: string
endTime: string
}
export interface CalendlyListUserBusyTimesResponse extends ToolResponse {
output: {
collection: Array<{
type: string
start_time: string
end_time: string
buffered_start_time?: string
buffered_end_time?: string
event?: {
uri: string
}
}>
}
}
export interface CalendlyListUserAvailabilitySchedulesParams {
apiKey: string
user: string
}
export interface CalendlyListUserAvailabilitySchedulesResponse extends ToolResponse {
output: {
collection: Array<{
uri: string
default: boolean
name: string
user: string
timezone: string
rules: Array<{
type: string
intervals: Array<{
from: string
to: string
}>
wday?: string
date?: string
}>
}>
}
}
export interface CalendlyCreateSchedulingLinkParams {
apiKey: string
eventTypeUri: string
}
export interface CalendlyCreateSchedulingLinkResponse extends ToolResponse {
output: {
resource: {
booking_url: string
owner: string
owner_type: string
}
}
}
export interface CalendlyCreateInviteeNoShowParams {
apiKey: string
inviteeUri: string
}
export interface CalendlyCreateInviteeNoShowResponse extends ToolResponse {
output: {
resource: {
uri: string
invitee: string
created_at: string
}
}
}
export interface CalendlyDeleteInviteeNoShowParams {
apiKey: string
noShowUuid: string
}
export interface CalendlyDeleteInviteeNoShowResponse extends ToolResponse {
output: {
deleted: boolean
message: string
}
}
export interface CalendlyListOrganizationMembershipsParams {
apiKey: string
organization?: string
user?: string
email?: string
role?: string
count?: number
pageToken?: string
}
export interface CalendlyListOrganizationMembershipsResponse extends ToolResponse {
output: {
collection: Array<{
uri: string
role: string
organization: string
created_at: string
updated_at: string
user: {
uri: string
name: string
slug: string
email: string
scheduling_url: string
timezone: string
time_notation: string
avatar_url: string | null
locale: string
created_at: string
updated_at: string
}
}>
pagination: CalendlyPagination
}
}
export interface CalendlyListRoutingFormsParams {
apiKey: string
organization: string
count?: number
pageToken?: string
sort?: string
}
export interface CalendlyListRoutingFormsResponse extends ToolResponse {
output: {
collection: Array<{
uri: string
organization: string
name: string
status: string
questions: Array<{
uuid: string
name: string
type: string
required: boolean
answer_choices: string[] | null
}>
created_at: string
updated_at: string
}>
pagination: CalendlyPagination
}
}
export interface CalendlyListRoutingFormSubmissionsParams {
apiKey: string
formUri: string
count?: number
pageToken?: string
sort?: string
}
export interface CalendlyListRoutingFormSubmissionsResponse extends ToolResponse {
output: {
collection: Array<{
uri: string
routing_form: string
submitter: string | null
submitter_type: string | null
created_at: string
updated_at: string
questions_and_answers: Array<{
question_uuid: string
question: string
answer: string
}>
tracking: {
utm_campaign: string | null
utm_source: string | null
utm_medium: string | null
utm_content: string | null
utm_term: string | null
salesforce_uuid: string | null
}
result: {
type: string
value: string
} | null
}>
pagination: CalendlyPagination
}
}
+43
View File
@@ -0,0 +1,43 @@
import { getErrorMessage } from '@sim/utils/errors'
/**
* Calendly addresses every resource both by bare UUID and by canonical URI, and users
* paste whichever one they have. Path segments need the UUID, query filters and request
* bodies need the URI, so each builder normalizes its input to the form the API expects.
*/
const CALENDLY_API_BASE = 'https://api.calendly.com'
/** Extracts the trailing UUID from a Calendly URI, or returns an already-bare UUID unchanged. */
export function toUuid(value: string): string {
const trimmed = value.trim().replace(/\/+$/, '')
return trimmed.includes('/') ? (trimmed.split('/').pop() as string) : trimmed
}
/** Expands a bare UUID into a canonical resource URI, or returns an already-full URI unchanged. */
export function toResourceUri(value: string, collection: string): string {
const trimmed = value.trim().replace(/\/+$/, '')
return trimmed.startsWith('http') ? trimmed : `${CALENDLY_API_BASE}/${collection}/${trimmed}`
}
/**
* `json` params reach a tool as a parsed array from the block, but as a raw JSON string when the
* tool is called directly from the registry, so array bodies normalize both shapes before sending.
*/
export function toStringArray(value: string[] | string | undefined, field: string): string[] {
if (value === undefined) return []
if (Array.isArray(value)) return value
let parsed: unknown
try {
parsed = JSON.parse(value)
} catch (error) {
throw new Error(`Invalid JSON input for ${field}: ${getErrorMessage(error)}`)
}
if (!Array.isArray(parsed)) {
throw new Error(`Expected ${field} to be an array of strings.`)
}
return parsed as string[]
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+24
View File
@@ -426,14 +426,26 @@ import {
} from '@/tools/calcom'
import {
calendlyCancelEventTool,
calendlyCreateEventInviteeTool,
calendlyCreateInviteeNoShowTool,
calendlyCreateSchedulingLinkTool,
calendlyCreateWebhookTool,
calendlyDeleteInviteeNoShowTool,
calendlyDeleteWebhookTool,
calendlyGetCurrentUserTool,
calendlyGetEventInviteeTool,
calendlyGetEventTypeTool,
calendlyGetScheduledEventTool,
calendlyGetUserTool,
calendlyListEventInviteesTool,
calendlyListEventTypeAvailableTimesTool,
calendlyListEventTypesTool,
calendlyListOrganizationMembershipsTool,
calendlyListRoutingFormSubmissionsTool,
calendlyListRoutingFormsTool,
calendlyListScheduledEventsTool,
calendlyListUserAvailabilitySchedulesTool,
calendlyListUserBusyTimesTool,
calendlyListWebhooksTool,
} from '@/tools/calendly'
import { clayPopulateTool } from '@/tools/clay'
@@ -6074,12 +6086,24 @@ export const tools: Record<string, ToolConfig> = {
tailscale_get_acl: tailscaleGetAclTool,
tailscale_set_acl: tailscaleSetAclTool,
calendly_get_current_user: calendlyGetCurrentUserTool,
calendly_get_user: calendlyGetUserTool,
calendly_list_event_types: calendlyListEventTypesTool,
calendly_get_event_type: calendlyGetEventTypeTool,
calendly_list_event_type_available_times: calendlyListEventTypeAvailableTimesTool,
calendly_list_scheduled_events: calendlyListScheduledEventsTool,
calendly_get_scheduled_event: calendlyGetScheduledEventTool,
calendly_list_event_invitees: calendlyListEventInviteesTool,
calendly_get_event_invitee: calendlyGetEventInviteeTool,
calendly_create_event_invitee: calendlyCreateEventInviteeTool,
calendly_cancel_event: calendlyCancelEventTool,
calendly_create_invitee_no_show: calendlyCreateInviteeNoShowTool,
calendly_delete_invitee_no_show: calendlyDeleteInviteeNoShowTool,
calendly_create_scheduling_link: calendlyCreateSchedulingLinkTool,
calendly_list_user_busy_times: calendlyListUserBusyTimesTool,
calendly_list_user_availability_schedules: calendlyListUserAvailabilitySchedulesTool,
calendly_list_organization_memberships: calendlyListOrganizationMembershipsTool,
calendly_list_routing_forms: calendlyListRoutingFormsTool,
calendly_list_routing_form_submissions: calendlyListRoutingFormSubmissionsTool,
calendly_list_webhooks: calendlyListWebhooksTool,
calendly_create_webhook: calendlyCreateWebhookTool,
calendly_delete_webhook: calendlyDeleteWebhookTool,