@hostwebhook/node-types 1.82.0 → 1.84.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,270 @@
1
+ /**
2
+ * El nodo de Airtable: leer y escribir records de una tabla.
3
+ *
4
+ * ## Por qué existe
5
+ *
6
+ * [2026-09-15] Airtable expone una API REST con OAuth 2.0 (PKCE obligatorio,
7
+ * refresh que rota) y webhooks. Es «la hoja de cálculo con tipos» que mucha
8
+ * gente usa de base de datos ligera, así que este nodo se parece más a
9
+ * Postgres/Sheets que a un servicio de mensajería: casi todo es CRUD sobre
10
+ * una tabla.
11
+ *
12
+ * ## Cómo se nombra todo
13
+ *
14
+ * Una base es `appXXXXXXXXXXXXXX`, una tabla `tblXXXXXXXXXXXXXX`, una vista
15
+ * `viwXXXXXXXXXXXXXX`, un record `recXXXXXXXXXXXXXX`. Base y tabla se eligen
16
+ * en desplegables que lee el esquema (`GET /v0/meta/bases`,
17
+ * `GET /v0/meta/bases/{baseId}/tables`), y **los campos se escriben por
18
+ * nombre**, que es como los ve el usuario en Airtable. El esquema trae tipo
19
+ * y opciones de cada campo, y el editor de campos los usa para ofrecer lo
20
+ * que cabe: las opciones de un select, el formato de una fecha.
21
+ *
22
+ * ## Uno o un lote
23
+ *
24
+ * Cada operación de escritura trabaja sobre UN record —el caso normal, con
25
+ * el Loop node repitiendo si hacen falta varios— y tiene en Advanced un
26
+ * «Batch» que acepta un array del payload. Airtable admite 10 por llamada y
27
+ * 5 llamadas por segundo por base; el servicio trocea y espera, el usuario
28
+ * no tiene que saberlo.
29
+ *
30
+ * ## Por qué ocho
31
+ *
32
+ * Las seis de records (listar, uno, crear, editar, upsert, borrar) más las
33
+ * dos de esquema que un flujo puede necesitar (bases, tablas y campos).
34
+ * Fuera a propósito: comentarios, crear tablas o campos, y los webhooks,
35
+ * que son del trigger.
36
+ *
37
+ * ⚠️ El orden de este array es el orden del desplegable.
38
+ */
39
+ export const AIRTABLE_OPERATIONS = [
40
+ "listRecords",
41
+ "getRecord",
42
+ "createRecord",
43
+ "updateRecord",
44
+ "upsertRecord",
45
+ "deleteRecord",
46
+ "listBases",
47
+ "getBaseSchema",
48
+ ];
49
+ export function isAirtableOperation(value) {
50
+ return (typeof value === "string" &&
51
+ AIRTABLE_OPERATIONS.includes(value));
52
+ }
53
+ /**
54
+ * Las que devuelven una COLECCIÓN sobre la que el nodo de abajo repite. Cada
55
+ * una itera SU campo (`iterateField`): `records`, `bases`, `tables`. La
56
+ * salida lleva `_meta.iterable: true`.
57
+ */
58
+ export const AIRTABLE_ITERABLE_OPERATIONS = [
59
+ "listRecords",
60
+ "listBases",
61
+ "getBaseSchema",
62
+ ];
63
+ const baseId = () => ({
64
+ name: "baseId",
65
+ label: "Base",
66
+ type: "base",
67
+ required: true,
68
+ description: "One of the bases the credential was granted access to.",
69
+ });
70
+ const tableId = () => ({
71
+ name: "tableId",
72
+ label: "Table",
73
+ type: "table",
74
+ required: true,
75
+ description: "A table in that base.",
76
+ });
77
+ const recordId = () => ({
78
+ name: "recordId",
79
+ label: "Record",
80
+ type: "template",
81
+ required: true,
82
+ placeholder: "{{payload.recordId}}",
83
+ description: "The record's ID (rec…), usually from an Airtable trigger or an earlier step.",
84
+ });
85
+ /* Los valores por nombre de campo. Cadenas con plantilla; el resto (números,
86
+ checkboxes, arrays de un multi-select o de linked records) como JSON. */
87
+ const fields = () => ({
88
+ name: "fields",
89
+ label: "Fields",
90
+ type: "fields",
91
+ description: "The values to write, by field name. Strings may use {{payload.x}}; select options, dates and linked records follow the field's type.",
92
+ });
93
+ /* `typecast` convierte "Sí" en un checkbox y "Done" en la opción de un
94
+ select aunque no exista todavía. Activado por defecto porque es lo que
95
+ uno espera al escribir desde un flujo; se puede apagar. */
96
+ const typecast = () => ({
97
+ name: "typecast",
98
+ label: "Convert values to the field's type",
99
+ type: "booleanSelect",
100
+ options: [
101
+ { value: "true", label: "Yes — text becomes the right type, new select options are created" },
102
+ { value: "false", label: "No — values must already match the field's type" },
103
+ ],
104
+ default: "true",
105
+ advanced: true,
106
+ description: "Airtable's typecast: with Yes, \"Yes\" fills a checkbox and an unknown option is added to a select.",
107
+ });
108
+ export const AIRTABLE_OPERATION_SPECS = {
109
+ listRecords: {
110
+ label: "List records",
111
+ description: "Records of a table, optionally through a view and a filter formula. Comes back as the table shows them: fields by name.",
112
+ apiRoute: "GET /v0/{baseId}/{tableId}",
113
+ iterateField: "records",
114
+ params: [
115
+ baseId(),
116
+ tableId(),
117
+ {
118
+ name: "viewId",
119
+ label: "View",
120
+ type: "view",
121
+ description: "Only the records in this view, in the view's order. Empty = the whole table.",
122
+ },
123
+ {
124
+ name: "filterByFormula",
125
+ label: "Filter formula",
126
+ type: "textarea",
127
+ placeholder: '{Status} = "Done"',
128
+ description: "An Airtable formula; only records where it is true come back. Field names in braces. Supports {{payload.x}}.",
129
+ },
130
+ {
131
+ name: "fields",
132
+ label: "Only these fields",
133
+ type: "fieldList",
134
+ advanced: true,
135
+ description: "Fields to include in each record. Empty = every field.",
136
+ },
137
+ {
138
+ name: "sortField",
139
+ label: "Sort by",
140
+ type: "field",
141
+ advanced: true,
142
+ description: "A field to order by. Empty = the view's order, or the table's.",
143
+ },
144
+ {
145
+ name: "sortDirection",
146
+ label: "Direction",
147
+ type: "select",
148
+ options: [
149
+ { value: "asc", label: "Ascending" },
150
+ { value: "desc", label: "Descending" },
151
+ ],
152
+ default: "asc",
153
+ advanced: true,
154
+ },
155
+ {
156
+ name: "maxRecords",
157
+ label: "Max records",
158
+ type: "number",
159
+ min: 1,
160
+ max: 1000,
161
+ default: 100,
162
+ advanced: true,
163
+ description: "How many to bring into the flow, at most (Airtable pages by 100; the node keeps asking). Default 100.",
164
+ },
165
+ ],
166
+ },
167
+ getRecord: {
168
+ label: "Get record",
169
+ description: "One record by its ID, with every field.",
170
+ apiRoute: "GET /v0/{baseId}/{tableId}/{recordId}",
171
+ params: [baseId(), tableId(), recordId()],
172
+ },
173
+ createRecord: {
174
+ label: "Create record",
175
+ description: "Adds a record with the values you give it. For many at once, put an array in Batch (Advanced) — Airtable takes 10 per call and the node paces it.",
176
+ apiRoute: "POST /v0/{baseId}/{tableId}",
177
+ params: [
178
+ baseId(),
179
+ tableId(),
180
+ fields(),
181
+ {
182
+ name: "records",
183
+ label: "Batch",
184
+ type: "json",
185
+ placeholder: "{{payload.rows}}",
186
+ advanced: true,
187
+ description: "An array of objects, each { \"Field\": value, … } — e.g. {{payload.rows}}. When set, Fields above is ignored.",
188
+ },
189
+ typecast(),
190
+ ],
191
+ },
192
+ updateRecord: {
193
+ label: "Update record",
194
+ description: "Changes the fields you give it and leaves the rest as they are. For many at once, put an array in Batch (Advanced).",
195
+ apiRoute: "PATCH /v0/{baseId}/{tableId}",
196
+ params: [
197
+ baseId(),
198
+ tableId(),
199
+ recordId(),
200
+ fields(),
201
+ {
202
+ name: "records",
203
+ label: "Batch",
204
+ type: "json",
205
+ placeholder: "{{payload.rows}}",
206
+ advanced: true,
207
+ description: "An array of { \"id\": \"rec…\", \"fields\": { … } } objects. When set, Record and Fields above are ignored.",
208
+ },
209
+ typecast(),
210
+ ],
211
+ },
212
+ upsertRecord: {
213
+ label: "Upsert record",
214
+ description: "Finds a record by the fields you merge on and updates it; creates it when there is none. Fails if more than one matches.",
215
+ apiRoute: "PATCH /v0/{baseId}/{tableId} (performUpsert)",
216
+ params: [
217
+ baseId(),
218
+ tableId(),
219
+ {
220
+ name: "mergeOn",
221
+ label: "Match on",
222
+ type: "fieldList",
223
+ required: true,
224
+ description: "One to three fields that identify the record — an email, an external ID. Text, number, select or date fields; not formulas.",
225
+ },
226
+ fields(),
227
+ {
228
+ name: "records",
229
+ label: "Batch",
230
+ type: "json",
231
+ placeholder: "{{payload.rows}}",
232
+ advanced: true,
233
+ description: "An array of objects, each { \"Field\": value, … }. When set, Fields above is ignored.",
234
+ },
235
+ typecast(),
236
+ ],
237
+ },
238
+ deleteRecord: {
239
+ label: "Delete record",
240
+ description: "Deletes a record. For many at once, put their IDs in Batch (Advanced).",
241
+ apiRoute: "DELETE /v0/{baseId}/{tableId}",
242
+ params: [
243
+ baseId(),
244
+ tableId(),
245
+ recordId(),
246
+ {
247
+ name: "recordIds",
248
+ label: "Batch",
249
+ type: "template",
250
+ placeholder: "{{payload.ids}}",
251
+ advanced: true,
252
+ description: "Record IDs to delete — an array from the payload, or comma-separated. When set, Record above is ignored.",
253
+ },
254
+ ],
255
+ },
256
+ listBases: {
257
+ label: "List bases",
258
+ description: "The bases this credential can reach, with their IDs and your permission level in each.",
259
+ apiRoute: "GET /v0/meta/bases",
260
+ iterateField: "bases",
261
+ params: [],
262
+ },
263
+ getBaseSchema: {
264
+ label: "Get base schema",
265
+ description: "The tables of a base with their fields (name, type, options) and views. What a flow reads before writing into a table it does not know.",
266
+ apiRoute: "GET /v0/meta/bases/{baseId}/tables",
267
+ iterateField: "tables",
268
+ params: [baseId()],
269
+ },
270
+ };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * El nodo de Calendly: lo que pasa alrededor de una reserva.
3
+ *
4
+ * ## Por qué existe
5
+ *
6
+ * [2026-09-15] Calendly expone una API v2 con OAuth 2.0 (PKCE + secret,
7
+ * refresh de un solo uso) y webhooks firmados. La reserva en sí la hace el
8
+ * invitado en Calendly; lo que HostWebhook necesita es leerla (el trigger la
9
+ * trae; este nodo la consulta), cancelarla, marcar el no-show, y las dos
10
+ * que cierran el círculo: un link de reserva que caduca al usarse y los
11
+ * huecos libres de un tipo de evento, para que un agente los ofrezca.
12
+ *
13
+ * ## Cómo se nombra todo
14
+ *
15
+ * Calendly identifica cada cosa por su URI completa
16
+ * (`https://api.calendly.com/scheduled_events/ABC…`), no por un id corto.
17
+ * Los parámetros que piden una la aceptan entera o sólo el uuid final; el
18
+ * servicio completa el prefijo. Los que piden «Mine / Whole organization»
19
+ * resuelven al `owner` y `organization` que la credencial guardó al
20
+ * conectar, así que el usuario no escribe URIs nunca.
21
+ *
22
+ * ## Por qué nueve
23
+ *
24
+ * Eventos (listar, uno, invitados, cancelar), no-show (poner y quitar),
25
+ * tipos de evento (listar), huecos libres y link de un solo uso. Fuera a
26
+ * propósito: crear tipos de evento, contactos, routing forms, recaps y toda
27
+ * la gestión de organización —nada de eso es «lo que pasa alrededor de una
28
+ * reserva». Los webhooks van en el trigger, no aquí.
29
+ *
30
+ * ⚠️ El orden de este array es el orden del desplegable.
31
+ */
32
+ export declare const CALENDLY_OPERATIONS: readonly ["listEvents", "getEvent", "listInvitees", "cancelEvent", "markNoShow", "undoNoShow", "listEventTypes", "getAvailableTimes", "createSchedulingLink"];
33
+ export type CalendlyOperation = (typeof CALENDLY_OPERATIONS)[number];
34
+ export declare function isCalendlyOperation(value: unknown): value is CalendlyOperation;
35
+ /**
36
+ * Las que devuelven una COLECCIÓN sobre la que el nodo de abajo repite. Cada
37
+ * una itera SU campo (`iterateField` en la spec): `events`, `invitees`,
38
+ * `eventTypes`, `slots`. La salida lleva `_meta.iterable: true`.
39
+ */
40
+ export declare const CALENDLY_ITERABLE_OPERATIONS: ReadonlyArray<CalendlyOperation>;
41
+ /**
42
+ * Tipos de parámetro. Los seis primeros son los de Apify. Los dos nuevos:
43
+ *
44
+ * - `datetime`: una fecha con hora, ISO 8601, en la zona horaria del
45
+ * usuario; el dashboard la pinta con el selector de Calendar y admite
46
+ * plantilla (`{{payload.startTime}}`).
47
+ * - `eventType`: un desplegable que lista los tipos de evento de la
48
+ * credencial (`GET /calendly-actions/event-types`); el valor es la URI del
49
+ * tipo. Admite plantilla para cuando viene del payload.
50
+ */
51
+ export type CalendlyParamType = "template" | "textarea" | "json" | "select" | "number" | "booleanSelect" | "datetime" | "eventType";
52
+ export interface CalendlyParamSpec {
53
+ name: string;
54
+ label: string;
55
+ type: CalendlyParamType;
56
+ required?: boolean;
57
+ description?: string;
58
+ placeholder?: string;
59
+ default?: string | number;
60
+ options?: ReadonlyArray<{
61
+ value: string;
62
+ label: string;
63
+ }>;
64
+ min?: number;
65
+ max?: number;
66
+ advanced?: boolean;
67
+ }
68
+ export interface CalendlyOperationSpec {
69
+ label: string;
70
+ description: string;
71
+ apiRoute: string;
72
+ params: CalendlyParamSpec[];
73
+ /** El campo de la salida que se itera; sólo en las iterables. */
74
+ iterateField?: string;
75
+ }
76
+ export declare const CALENDLY_OPERATION_SPECS: Record<CalendlyOperation, CalendlyOperationSpec>;
@@ -0,0 +1,285 @@
1
+ /**
2
+ * El nodo de Calendly: lo que pasa alrededor de una reserva.
3
+ *
4
+ * ## Por qué existe
5
+ *
6
+ * [2026-09-15] Calendly expone una API v2 con OAuth 2.0 (PKCE + secret,
7
+ * refresh de un solo uso) y webhooks firmados. La reserva en sí la hace el
8
+ * invitado en Calendly; lo que HostWebhook necesita es leerla (el trigger la
9
+ * trae; este nodo la consulta), cancelarla, marcar el no-show, y las dos
10
+ * que cierran el círculo: un link de reserva que caduca al usarse y los
11
+ * huecos libres de un tipo de evento, para que un agente los ofrezca.
12
+ *
13
+ * ## Cómo se nombra todo
14
+ *
15
+ * Calendly identifica cada cosa por su URI completa
16
+ * (`https://api.calendly.com/scheduled_events/ABC…`), no por un id corto.
17
+ * Los parámetros que piden una la aceptan entera o sólo el uuid final; el
18
+ * servicio completa el prefijo. Los que piden «Mine / Whole organization»
19
+ * resuelven al `owner` y `organization` que la credencial guardó al
20
+ * conectar, así que el usuario no escribe URIs nunca.
21
+ *
22
+ * ## Por qué nueve
23
+ *
24
+ * Eventos (listar, uno, invitados, cancelar), no-show (poner y quitar),
25
+ * tipos de evento (listar), huecos libres y link de un solo uso. Fuera a
26
+ * propósito: crear tipos de evento, contactos, routing forms, recaps y toda
27
+ * la gestión de organización —nada de eso es «lo que pasa alrededor de una
28
+ * reserva». Los webhooks van en el trigger, no aquí.
29
+ *
30
+ * ⚠️ El orden de este array es el orden del desplegable.
31
+ */
32
+ export const CALENDLY_OPERATIONS = [
33
+ "listEvents",
34
+ "getEvent",
35
+ "listInvitees",
36
+ "cancelEvent",
37
+ "markNoShow",
38
+ "undoNoShow",
39
+ "listEventTypes",
40
+ "getAvailableTimes",
41
+ "createSchedulingLink",
42
+ ];
43
+ export function isCalendlyOperation(value) {
44
+ return (typeof value === "string" &&
45
+ CALENDLY_OPERATIONS.includes(value));
46
+ }
47
+ /**
48
+ * Las que devuelven una COLECCIÓN sobre la que el nodo de abajo repite. Cada
49
+ * una itera SU campo (`iterateField` en la spec): `events`, `invitees`,
50
+ * `eventTypes`, `slots`. La salida lleva `_meta.iterable: true`.
51
+ */
52
+ export const CALENDLY_ITERABLE_OPERATIONS = [
53
+ "listEvents",
54
+ "listInvitees",
55
+ "listEventTypes",
56
+ "getAvailableTimes",
57
+ ];
58
+ /* «Mine» es el usuario que conectó; «Whole organization» exige ser admin u
59
+ owner en Calendly, y si no lo es la API contesta 403 y el nodo lo dice. */
60
+ const owner = () => ({
61
+ name: "owner",
62
+ label: "Whose",
63
+ type: "select",
64
+ options: [
65
+ { value: "mine", label: "Mine — the connected user" },
66
+ { value: "organization", label: "Whole organization (admins only)" },
67
+ ],
68
+ default: "mine",
69
+ description: "Mine uses the account that connected the credential. Whole organization needs an organization admin or owner.",
70
+ });
71
+ /* Un evento se nombra por su URI o por el uuid del final; da igual. */
72
+ const eventUri = () => ({
73
+ name: "eventUri",
74
+ label: "Event",
75
+ type: "template",
76
+ required: true,
77
+ placeholder: "{{payload.event.uri}}",
78
+ description: "The scheduled event — its URI, or just the ID at the end of it. Usually {{payload.event.uri}} from a Calendly trigger.",
79
+ });
80
+ const eventType = () => ({
81
+ name: "eventType",
82
+ label: "Event type",
83
+ type: "eventType",
84
+ required: true,
85
+ description: "One of the event types on the connected account (e.g. “30 min intro call”).",
86
+ });
87
+ const pageLimit = (max, fallback) => ({
88
+ name: "limit",
89
+ label: "Max results",
90
+ type: "number",
91
+ min: 1,
92
+ max,
93
+ default: fallback,
94
+ advanced: true,
95
+ description: `How many to bring into the flow, at most. Default ${fallback}.`,
96
+ });
97
+ export const CALENDLY_OPERATION_SPECS = {
98
+ listEvents: {
99
+ label: "List events",
100
+ description: "Scheduled events in a time window — yours or the whole organization's. Each one comes with its event type, hosts and location; invitees are a separate step.",
101
+ apiRoute: "GET /scheduled_events",
102
+ iterateField: "events",
103
+ params: [
104
+ owner(),
105
+ {
106
+ name: "status",
107
+ label: "Status",
108
+ type: "select",
109
+ options: [
110
+ { value: "active", label: "Active — upcoming and past, not canceled" },
111
+ { value: "canceled", label: "Canceled" },
112
+ { value: "all", label: "All" },
113
+ ],
114
+ default: "active",
115
+ },
116
+ {
117
+ name: "minStartTime",
118
+ label: "Starting from",
119
+ type: "datetime",
120
+ placeholder: "{{payload.from}}",
121
+ description: "Only events that start at or after this moment. Empty = no lower bound.",
122
+ },
123
+ {
124
+ name: "maxStartTime",
125
+ label: "Starting before",
126
+ type: "datetime",
127
+ placeholder: "{{payload.to}}",
128
+ description: "Only events that start before this moment. Empty = no upper bound.",
129
+ },
130
+ {
131
+ name: "inviteeEmail",
132
+ label: "Invitee email",
133
+ type: "template",
134
+ placeholder: "{{payload.email}}",
135
+ advanced: true,
136
+ description: "Only events booked by this email. Empty = everyone.",
137
+ },
138
+ {
139
+ name: "sort",
140
+ label: "Order",
141
+ type: "select",
142
+ options: [
143
+ { value: "start_time:asc", label: "Soonest first" },
144
+ { value: "start_time:desc", label: "Latest first" },
145
+ ],
146
+ default: "start_time:asc",
147
+ advanced: true,
148
+ },
149
+ pageLimit(500, 100),
150
+ ],
151
+ },
152
+ getEvent: {
153
+ label: "Get event",
154
+ description: "One scheduled event by its URI: name, start and end, event type, hosts, location, and how many invitees it has.",
155
+ apiRoute: "GET /scheduled_events/{uuid}",
156
+ params: [eventUri()],
157
+ },
158
+ listInvitees: {
159
+ label: "List invitees",
160
+ description: "Who booked an event: name, email, timezone, their answers to the booking questions, and whether they canceled or rescheduled.",
161
+ apiRoute: "GET /scheduled_events/{uuid}/invitees",
162
+ iterateField: "invitees",
163
+ params: [
164
+ eventUri(),
165
+ {
166
+ name: "status",
167
+ label: "Status",
168
+ type: "select",
169
+ options: [
170
+ { value: "active", label: "Active" },
171
+ { value: "canceled", label: "Canceled" },
172
+ { value: "all", label: "All" },
173
+ ],
174
+ default: "all",
175
+ advanced: true,
176
+ },
177
+ {
178
+ name: "inviteeEmail",
179
+ label: "Invitee email",
180
+ type: "template",
181
+ placeholder: "{{payload.email}}",
182
+ advanced: true,
183
+ description: "Only the invitee with this email. Empty = all of them.",
184
+ },
185
+ pageLimit(100, 100),
186
+ ],
187
+ },
188
+ cancelEvent: {
189
+ label: "Cancel event",
190
+ description: "Cancels a scheduled event and notifies its invitees, the same way the host would from Calendly.",
191
+ apiRoute: "POST /scheduled_events/{uuid}/cancellation",
192
+ params: [
193
+ eventUri(),
194
+ {
195
+ name: "reason",
196
+ label: "Reason",
197
+ type: "textarea",
198
+ placeholder: "Sorry — something came up. Please rebook with the link below.",
199
+ description: "Shown to the invitees in the cancellation email. Supports {{payload.x}}. Empty = no reason.",
200
+ },
201
+ ],
202
+ },
203
+ markNoShow: {
204
+ label: "Mark invitee as no-show",
205
+ description: "Marks an invitee as a no-show on a past event. Returns the no-show record; keep its URI if you may need to undo it.",
206
+ apiRoute: "POST /invitee_no_shows",
207
+ params: [
208
+ {
209
+ name: "inviteeUri",
210
+ label: "Invitee",
211
+ type: "template",
212
+ required: true,
213
+ placeholder: "{{payload.invitee.uri}}",
214
+ description: "The invitee — its URI, from List invitees or a Calendly trigger.",
215
+ },
216
+ ],
217
+ },
218
+ undoNoShow: {
219
+ label: "Undo no-show",
220
+ description: "Removes a no-show mark. Takes the URI that Mark invitee as no-show returned.",
221
+ apiRoute: "DELETE /invitee_no_shows/{uuid}",
222
+ params: [
223
+ {
224
+ name: "noShowUri",
225
+ label: "No-show",
226
+ type: "template",
227
+ required: true,
228
+ placeholder: "{{payload.uri}}",
229
+ description: "The no-show record to remove — its URI, or just the ID at the end.",
230
+ },
231
+ ],
232
+ },
233
+ listEventTypes: {
234
+ label: "List event types",
235
+ description: "The event types you (or the organization) offer: name, duration, scheduling URL, and whether they are active. What you need before asking for available times or a link.",
236
+ apiRoute: "GET /event_types",
237
+ iterateField: "eventTypes",
238
+ params: [
239
+ owner(),
240
+ {
241
+ name: "activeOnly",
242
+ label: "Active only",
243
+ type: "booleanSelect",
244
+ options: [
245
+ { value: "true", label: "Yes — only event types that can be booked" },
246
+ { value: "false", label: "No — include hidden and inactive ones" },
247
+ ],
248
+ default: "true",
249
+ advanced: true,
250
+ },
251
+ pageLimit(100, 100),
252
+ ],
253
+ },
254
+ getAvailableTimes: {
255
+ label: "Get available times",
256
+ description: "The free slots of an event type in a window of up to 7 days, each with its start time and a scheduling URL that books exactly that slot. Made for an agent that offers times.",
257
+ apiRoute: "GET /event_type_available_times",
258
+ iterateField: "slots",
259
+ params: [
260
+ eventType(),
261
+ {
262
+ name: "startTime",
263
+ label: "From",
264
+ type: "datetime",
265
+ required: true,
266
+ placeholder: "{{payload.from}}",
267
+ description: "Start of the window. Must be in the future.",
268
+ },
269
+ {
270
+ name: "endTime",
271
+ label: "To",
272
+ type: "datetime",
273
+ required: true,
274
+ placeholder: "{{payload.to}}",
275
+ description: "End of the window — at most 7 days after From. A longer window is cut to 7 days.",
276
+ },
277
+ ],
278
+ },
279
+ createSchedulingLink: {
280
+ label: "Create single-use scheduling link",
281
+ description: "A booking link for one event type that stops working after one booking. Send it to someone instead of your public link.",
282
+ apiRoute: "POST /scheduling_links",
283
+ params: [eventType()],
284
+ },
285
+ };
@@ -75,6 +75,8 @@ export const NODE_CONNECTIONS = {
75
75
  jiraAction: { acceptsInputFrom: ACTION_INPUTS, canOutputTo: PROCESSING_OUTPUTS },
76
76
  bucketAction: { acceptsInputFrom: ACTION_INPUTS, canOutputTo: PROCESSING_OUTPUTS },
77
77
  apifyAction: { acceptsInputFrom: ACTION_INPUTS, canOutputTo: PROCESSING_OUTPUTS },
78
+ calendlyAction: { acceptsInputFrom: ACTION_INPUTS, canOutputTo: PROCESSING_OUTPUTS },
79
+ airtableAction: { acceptsInputFrom: ACTION_INPUTS, canOutputTo: PROCESSING_OUTPUTS },
78
80
  googleContactsAction: { acceptsInputFrom: ACTION_INPUTS, canOutputTo: PROCESSING_OUTPUTS },
79
81
  googleAnalyticsAction: { acceptsInputFrom: ACTION_INPUTS, canOutputTo: PROCESSING_OUTPUTS },
80
82
  notionAction: { acceptsInputFrom: ACTION_INPUTS, canOutputTo: PROCESSING_OUTPUTS },
@@ -72,6 +72,12 @@ export declare const CREDENTIAL_TYPES: readonly [{
72
72
  readonly type: "firecrawl";
73
73
  }, {
74
74
  readonly type: "apify";
75
+ }, {
76
+ readonly type: "calendly_oauth2";
77
+ }, {
78
+ readonly type: "calendly";
79
+ }, {
80
+ readonly type: "airtable_oauth2";
75
81
  }, {
76
82
  readonly type: "http_auth";
77
83
  }, {
@@ -53,6 +53,26 @@ export const CREDENTIAL_TYPES = [
53
53
  /* [2026-09-13] Apify no tiene OAuth para terceros: es un API token, como
54
54
  Firecrawl. Lo lee el nodo `apifyAction`. */
55
55
  { type: 'apify' },
56
+ /* [2026-09-15] El OAuth de HostWebhook contra Calendly. Se anota ANTES de
57
+ escribir el servicio, por lo que cuenta el bloque de Mailchimp de abajo:
58
+ sin esta línea el `create` de la credencial muere en runtime con un enum
59
+ de Mongoose que TypeScript no ve. Lleva `_oauth2` como Mailchimp y
60
+ Shopify. Guarda, además de los tokens, el `owner` y la `organization`
61
+ (URIs) que el token endpoint devuelve: cada lista y cada webhook los
62
+ necesita. El refresh token es de UN solo uso: se sobreescribe en el
63
+ mismo write que lo gasta. */
64
+ { type: 'calendly_oauth2' },
65
+ /* El personal access token de Calendly (Integrations → API & webhooks),
66
+ para quien no quiera OAuth. Va aparte de `calendly_oauth2` por lo mismo
67
+ que `discord_bot` va aparte de `discord_oauth`: lo que guardan es
68
+ distinto (aquí un secreto suyo que no caduca y ninguna URI hasta que se
69
+ valida con `GET /users/me`). */
70
+ { type: 'calendly' },
71
+ /* El OAuth de HostWebhook contra Airtable. PKCE obligatorio, secret por
72
+ Basic auth, access token de 60 minutos y refresh de 60 días que rota
73
+ en cada uso. El usuario elige en la pantalla de Airtable QUÉ bases
74
+ concede; los desplegables sólo listan ésas. */
75
+ { type: 'airtable_oauth2' },
56
76
  { type: 'http_auth' },
57
77
  { type: 'aws_s3' },
58
78
  /* [2026-09-14] `memory_contextwindow` se retiró: el backend de memoria