@agentuity/schedule 3.0.12 → 3.1.1

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.
package/src/service.ts ADDED
@@ -0,0 +1,833 @@
1
+ import { FetchAdapter, buildUrl, toServiceException } from '@agentuity/adapter';
2
+ import { z } from 'zod';
3
+
4
+ /**
5
+ * A scheduled job that fires at intervals defined by a cron expression.
6
+ *
7
+ * Schedules are the top-level resource. Each schedule has one or more
8
+ * {@link ScheduleDestination | destinations} that receive HTTP callbacks
9
+ * when the schedule fires.
10
+ */
11
+ export const ScheduleSchema = z.object({
12
+ /**
13
+ * Unique identifier for the schedule.
14
+ *
15
+ * @remarks Prefixed with `sch_`.
16
+ */
17
+ id: z.string().describe('Unique identifier for the schedule.'),
18
+
19
+ /**
20
+ * ISO 8601 timestamp when the schedule was created.
21
+ */
22
+ created_at: z.string().describe('ISO 8601 timestamp when the schedule was created.'),
23
+
24
+ /**
25
+ * ISO 8601 timestamp when the schedule was last modified.
26
+ */
27
+ updated_at: z.string().describe('ISO 8601 timestamp when the schedule was last modified.'),
28
+
29
+ /**
30
+ * ID of the user who created the schedule.
31
+ */
32
+ created_by: z.string().describe('ID of the user who created the schedule.'),
33
+
34
+ /**
35
+ * Human-readable name for the schedule.
36
+ */
37
+ name: z.string().describe('Human-readable name for the schedule.'),
38
+
39
+ /**
40
+ * Optional description of the schedule's purpose.
41
+ */
42
+ description: z.string().nullable().describe("Optional description of the schedule's purpose."),
43
+
44
+ /**
45
+ * A cron expression defining the schedule's firing interval
46
+ * (e.g., `'0 9 * * 1-5'` for weekdays at 9 AM).
47
+ *
48
+ * @remarks Validated on creation and update by the server.
49
+ * Supports standard five-field cron syntax including step values (e.g., every 5 minutes).
50
+ */
51
+ expression: z.string().describe("A cron expression defining the schedule's firing interval"),
52
+
53
+ /**
54
+ * ISO 8601 timestamp of the next scheduled execution.
55
+ *
56
+ * @remarks Automatically computed from the cron expression. Updated each time
57
+ * the schedule fires or the expression is changed.
58
+ */
59
+ due_date: z.string().describe('ISO 8601 timestamp of the next scheduled execution.'),
60
+
61
+ /**
62
+ * Whether this is a system-managed schedule.
63
+ *
64
+ * @remarks Internal schedules are created by the system and cannot be modified
65
+ * or deleted by users.
66
+ */
67
+ internal: z
68
+ .boolean()
69
+ .describe(
70
+ 'Whether this is a system-managed schedule. Internal schedules cannot be modified or deleted users.'
71
+ ),
72
+ });
73
+
74
+ export type Schedule = z.infer<typeof ScheduleSchema>;
75
+
76
+ /**
77
+ * A delivery target for a scheduled job.
78
+ *
79
+ * When the schedule fires, an HTTP request is sent to each of its destinations.
80
+ */
81
+ export const ScheduleDestinationSchema = z.object({
82
+ /**
83
+ * Unique identifier for the destination.
84
+ *
85
+ * @remarks Prefixed with `sdst_`.
86
+ */
87
+ id: z.string().describe('Unique identifier for the destination.'),
88
+
89
+ /**
90
+ * The ID of the parent schedule.
91
+ */
92
+ schedule_id: z.string().describe('The ID of the parent schedule.'),
93
+
94
+ /**
95
+ * ISO 8601 timestamp when the destination was created.
96
+ */
97
+ created_at: z.string().describe('ISO 8601 timestamp when the destination was created.'),
98
+
99
+ /**
100
+ * ISO 8601 timestamp when the destination was last updated.
101
+ */
102
+ updated_at: z.string().describe('ISO 8601 timestamp when the destination was last updated.'),
103
+
104
+ /**
105
+ * ID of the user who created the destination.
106
+ */
107
+ created_by: z.string().describe('ID of the user who created the destination.'),
108
+
109
+ /**
110
+ * The destination type: `'url'` for HTTP endpoints or `'sandbox'` for Agentuity sandbox execution.
111
+ */
112
+ type: z
113
+ .enum(['url', 'sandbox'])
114
+ .describe(
115
+ "The destination type: `'url'` for HTTP endpoints or `'sandbox'` for Agentuity sandbox execution."
116
+ ),
117
+
118
+ /**
119
+ * Type-specific destination configuration.
120
+ *
121
+ * @remarks
122
+ * For `'url'` type:
123
+ * ```typescript
124
+ * { url: string; headers?: Record<string, string>; method?: string }
125
+ * ```
126
+ * For `'sandbox'` type: sandbox-specific configuration.
127
+ */
128
+ config: z.record(z.string(), z.unknown()).describe('Type-specific destination configuration.'),
129
+ });
130
+
131
+ export type ScheduleDestination = z.infer<typeof ScheduleDestinationSchema>;
132
+
133
+ /**
134
+ * A record of a single delivery attempt for a scheduled execution.
135
+ *
136
+ * Each time a schedule fires, one delivery record is created per destination.
137
+ * Failed deliveries may be retried automatically.
138
+ */
139
+ export const ScheduleDeliverySchema = z.object({
140
+ /**
141
+ * Unique identifier for the delivery attempt.
142
+ */
143
+ id: z.string().describe('Unique identifier for the delivery attempt.'),
144
+
145
+ /**
146
+ * ISO 8601 timestamp when the delivery was attempted.
147
+ */
148
+ date: z.string().describe('ISO 8601 timestamp when the delivery was attempted.'),
149
+
150
+ /**
151
+ * The ID of the schedule that triggered this delivery.
152
+ */
153
+ schedule_id: z.string().describe('The ID of the schedule that triggered this delivery.'),
154
+
155
+ /**
156
+ * The ID of the destination this delivery was sent to.
157
+ */
158
+ schedule_destination_id: z
159
+ .string()
160
+ .describe('The ID of the destination this delivery was sent to.'),
161
+
162
+ /**
163
+ * Delivery status: `'pending'` (queued), `'success'` (delivered), or `'failed'` (delivery error).
164
+ */
165
+ status: z
166
+ .enum(['pending', 'success', 'failed'])
167
+ .describe(
168
+ "Delivery status: `'pending'` (queued), `'success'` (delivered), or `'failed'` (delivery error)."
169
+ ),
170
+
171
+ /**
172
+ * Number of retry attempts made for this delivery.
173
+ */
174
+ retries: z.number().describe('Number of retry attempts made for this delivery.'),
175
+
176
+ /**
177
+ * Error message if the delivery failed, `null` on success.
178
+ */
179
+ error: z
180
+ .string()
181
+ .nullable()
182
+ .describe('Error message if the delivery failed, `null` on success.'),
183
+
184
+ /**
185
+ * The response received from the destination, or `null` if no response.
186
+ */
187
+ response: z
188
+ .record(z.string(), z.unknown())
189
+ .nullable()
190
+ .describe('The response received from the destination, or `null` if no response.'),
191
+ });
192
+
193
+ export type ScheduleDelivery = z.infer<typeof ScheduleDeliverySchema>;
194
+
195
+ /**
196
+ * Parameters for creating a new schedule.
197
+ */
198
+ export const CreateScheduleParamsSchema = z.object({
199
+ /**
200
+ * Human-readable name for the schedule.
201
+ */
202
+ name: z.string().describe('Human-readable name for the schedule.'),
203
+
204
+ /**
205
+ * Optional description of the schedule's purpose.
206
+ */
207
+ description: z.string().optional().describe("Optional description of the schedule's purpose."),
208
+
209
+ /**
210
+ * Cron expression defining when the schedule fires
211
+ * (e.g., `'0 * * * *'` for every hour).
212
+ *
213
+ * @remarks Must be a valid cron expression. Validated by the server on creation.
214
+ * Supports standard five-field cron syntax including step values.
215
+ */
216
+ expression: z.string().describe('Cron expression defining when the schedule fires'),
217
+
218
+ /**
219
+ * Whether this is a system-managed schedule.
220
+ *
221
+ * @remarks Internal schedules are created by the system for workflows and cannot
222
+ * be modified or deleted directly by users.
223
+ */
224
+ internal: z.boolean().optional().describe('Whether this is a system-managed schedule.'),
225
+
226
+ /**
227
+ * Optional array of destinations to create alongside the schedule.
228
+ *
229
+ * @remarks Destinations are created atomically with the schedule — if any destination
230
+ * fails validation, the entire creation is rolled back.
231
+ */
232
+ destinations: z
233
+ .array(z.lazy(() => CreateScheduleDestinationParamsSchema))
234
+ .optional()
235
+ .describe('Optional array of destinations to create alongside the schedule.'),
236
+ });
237
+
238
+ export type CreateScheduleParams = z.infer<typeof CreateScheduleParamsSchema>;
239
+
240
+ /**
241
+ * Parameters for creating a new destination on a schedule.
242
+ */
243
+ export const CreateScheduleDestinationParamsSchema = z.object({
244
+ /**
245
+ * The destination type: `'url'` for HTTP endpoints or `'sandbox'` for Agentuity sandbox execution.
246
+ */
247
+ type: z
248
+ .enum(['url', 'sandbox'])
249
+ .describe(
250
+ "The destination type: `'url'` for HTTP endpoints or `'sandbox'` for Agentuity sandbox execution."
251
+ ),
252
+
253
+ /**
254
+ * Type-specific destination configuration.
255
+ *
256
+ * @remarks
257
+ * For `'url'` type:
258
+ * ```typescript
259
+ * { url: string; headers?: Record<string, string>; method?: string }
260
+ * ```
261
+ * For `'sandbox'` type: sandbox-specific configuration.
262
+ */
263
+ config: z.record(z.string(), z.unknown()).describe('Type-specific destination configuration.'),
264
+ });
265
+
266
+ export type CreateScheduleDestinationParams = z.infer<typeof CreateScheduleDestinationParamsSchema>;
267
+
268
+ /**
269
+ * Parameters for updating an existing schedule.
270
+ *
271
+ * All fields are optional; only provided fields are updated.
272
+ */
273
+ export const UpdateScheduleParamsSchema = z.object({
274
+ /**
275
+ * Updated human-readable name for the schedule.
276
+ */
277
+ name: z.string().optional().describe('Updated human-readable name for the schedule.'),
278
+
279
+ /**
280
+ * Updated description.
281
+ */
282
+ description: z.string().optional().describe('Updated description.'),
283
+
284
+ /**
285
+ * Updated cron expression. If changed, the `due_date` is automatically recomputed.
286
+ *
287
+ * @remarks Must be a valid cron expression. Validated by the server on update.
288
+ */
289
+ expression: z
290
+ .string()
291
+ .optional()
292
+ .describe('Updated cron expression. If changed, the `due_date` is automatically recomputed.'),
293
+ });
294
+
295
+ export type UpdateScheduleParams = z.infer<typeof UpdateScheduleParamsSchema>;
296
+
297
+ /**
298
+ * Paginated list of schedules.
299
+ */
300
+ export const ScheduleListResultSchema = z.object({
301
+ /**
302
+ * Array of schedule records for the current page.
303
+ */
304
+ schedules: z.array(ScheduleSchema).describe('Array of schedule records for the current page.'),
305
+
306
+ /**
307
+ * Total number of schedules across all pages.
308
+ */
309
+ total: z.number().describe('Total number of schedules across all pages.'),
310
+ });
311
+
312
+ export type ScheduleListResult = z.infer<typeof ScheduleListResultSchema>;
313
+
314
+ /**
315
+ * A schedule with its associated destinations.
316
+ */
317
+ export const ScheduleGetResultSchema = z.object({
318
+ /**
319
+ * The schedule record.
320
+ */
321
+ schedule: ScheduleSchema.describe('The schedule record.'),
322
+
323
+ /**
324
+ * Array of destinations configured for this schedule.
325
+ */
326
+ destinations: z
327
+ .array(ScheduleDestinationSchema)
328
+ .describe('Array of destinations configured for this schedule.'),
329
+ });
330
+
331
+ export type ScheduleGetResult = z.infer<typeof ScheduleGetResultSchema>;
332
+
333
+ /**
334
+ * Result of creating a schedule, including any destinations created atomically.
335
+ */
336
+ export const ScheduleCreateResultSchema = z.object({
337
+ /**
338
+ * The newly created schedule record.
339
+ */
340
+ schedule: ScheduleSchema.describe('The newly created schedule record.'),
341
+
342
+ /**
343
+ * Array of destinations that were created alongside the schedule.
344
+ */
345
+ destinations: z
346
+ .array(ScheduleDestinationSchema)
347
+ .describe('Array of destinations that were created alongside the schedule.'),
348
+ });
349
+
350
+ export type ScheduleCreateResult = z.infer<typeof ScheduleCreateResultSchema>;
351
+
352
+ /**
353
+ * List of delivery attempts for a schedule.
354
+ */
355
+ export const ScheduleDeliveryListResultSchema = z.object({
356
+ /**
357
+ * Array of delivery attempt records.
358
+ */
359
+ deliveries: z.array(ScheduleDeliverySchema).describe('Array of delivery attempt records.'),
360
+ });
361
+
362
+ export type ScheduleDeliveryListResult = z.infer<typeof ScheduleDeliveryListResultSchema>;
363
+
364
+ /**
365
+ * Client for the Agentuity Schedule service.
366
+ *
367
+ * Provides methods for creating, managing, and monitoring cron-based scheduled jobs.
368
+ * Schedules fire at intervals defined by cron expressions and deliver HTTP requests
369
+ * to configured destinations (URLs or sandboxes).
370
+ *
371
+ * The service supports:
372
+ * - CRUD operations on schedules and their destinations
373
+ * - Cron expression validation
374
+ * - Delivery history and monitoring
375
+ * - Automatic due date computation from cron expressions
376
+ *
377
+ * All methods are instrumented with OpenTelemetry spans for observability.
378
+ *
379
+ * @example
380
+ * ```typescript
381
+ * const schedules = new ScheduleService(baseUrl, adapter);
382
+ *
383
+ * // Create a schedule that fires every hour
384
+ * const result = await schedules.create({
385
+ * name: 'Hourly Sync',
386
+ * expression: '0 * * * *',
387
+ * destinations: [{ type: 'url', config: { url: 'https://example.com/sync' } }],
388
+ * });
389
+ *
390
+ * // List all schedules
391
+ * const { schedules: list, total } = await schedules.list();
392
+ * ```
393
+ */
394
+ export class ScheduleService {
395
+ #adapter: FetchAdapter;
396
+ #baseUrl: string;
397
+
398
+ /**
399
+ * Create a new ScheduleService instance.
400
+ *
401
+ * @param baseUrl - The base URL for the Agentuity Schedule API (e.g., `https://api.agentuity.com`)
402
+ * @param adapter - The HTTP fetch adapter used for making API requests
403
+ */
404
+ constructor(baseUrl: string, adapter: FetchAdapter) {
405
+ this.#adapter = adapter;
406
+ this.#baseUrl = baseUrl;
407
+ }
408
+
409
+ /**
410
+ * Create a new schedule with optional destinations.
411
+ *
412
+ * The schedule and its destinations are created atomically — if any validation
413
+ * fails, the entire operation is rolled back.
414
+ *
415
+ * @param params - The schedule creation parameters including name, cron expression, and optional destinations
416
+ * @returns The created schedule and its destinations
417
+ * @throws ServiceException on API errors (e.g., invalid cron expression, invalid destination URL)
418
+ *
419
+ * @example
420
+ * ```typescript
421
+ * const result = await schedules.create({
422
+ * name: 'Daily Report',
423
+ * description: 'Generate and send daily reports',
424
+ * expression: '0 9 * * *',
425
+ * destinations: [
426
+ * { type: 'url', config: { url: 'https://example.com/reports' } },
427
+ * ],
428
+ * });
429
+ * console.log('Created schedule:', result.schedule.id);
430
+ * console.log('Next run:', result.schedule.due_date);
431
+ * ```
432
+ */
433
+ async create(params: CreateScheduleParams): Promise<ScheduleCreateResult> {
434
+ const url = buildUrl(this.#baseUrl, '/schedule/create');
435
+ const signal = AbortSignal.timeout(30_000);
436
+ const res = await this.#adapter.invoke<ScheduleCreateResult>(url, {
437
+ method: 'POST',
438
+ signal,
439
+ body: JSON.stringify(params),
440
+ contentType: 'application/json',
441
+ telemetry: {
442
+ name: 'agentuity.schedule.create',
443
+ attributes: {
444
+ destinationCount: String(params.destinations?.length ?? 0),
445
+ name: params.name,
446
+ },
447
+ },
448
+ });
449
+
450
+ if (res.ok) {
451
+ return res.data;
452
+ }
453
+
454
+ throw await toServiceException('POST', url, res.response);
455
+ }
456
+
457
+ /**
458
+ * List all schedules with optional pagination.
459
+ *
460
+ * @param params - Optional pagination parameters
461
+ * @param params.limit - Maximum number of schedules to return (max 500)
462
+ * @param params.offset - Number of schedules to skip for pagination
463
+ * @returns A paginated list of schedules with the total count
464
+ * @throws ServiceException on API errors
465
+ *
466
+ * @example
467
+ * ```typescript
468
+ * // List first page
469
+ * const { schedules, total } = await service.list({ limit: 20 });
470
+ * console.log(`Showing ${schedules.length} of ${total} schedules`);
471
+ *
472
+ * // Paginate through all schedules
473
+ * const page2 = await service.list({ limit: 20, offset: 20 });
474
+ * ```
475
+ */
476
+ async list(params?: { limit?: number; offset?: number }): Promise<ScheduleListResult> {
477
+ const qs = new URLSearchParams();
478
+ if (params?.limit !== undefined) {
479
+ qs.set('limit', String(params.limit));
480
+ }
481
+ if (params?.offset !== undefined) {
482
+ qs.set('offset', String(params.offset));
483
+ }
484
+
485
+ const path = qs.toString() ? `/schedule/list?${qs.toString()}` : '/schedule/list';
486
+ const url = buildUrl(this.#baseUrl, path);
487
+ const signal = AbortSignal.timeout(30_000);
488
+ const res = await this.#adapter.invoke<ScheduleListResult>(url, {
489
+ method: 'GET',
490
+ signal,
491
+ telemetry: {
492
+ name: 'agentuity.schedule.list',
493
+ attributes: {
494
+ limit: String(params?.limit ?? ''),
495
+ offset: String(params?.offset ?? ''),
496
+ },
497
+ },
498
+ });
499
+
500
+ if (res.ok) {
501
+ return res.data;
502
+ }
503
+
504
+ throw await toServiceException('GET', url, res.response);
505
+ }
506
+
507
+ /**
508
+ * Get a schedule by its ID, including all configured destinations.
509
+ *
510
+ * @param scheduleId - The schedule ID (prefixed with `sch_`)
511
+ * @returns The schedule record and its array of destinations
512
+ * @throws ServiceException on API errors (including 404 if not found)
513
+ *
514
+ * @example
515
+ * ```typescript
516
+ * const { schedule, destinations } = await service.get('sch_abc123');
517
+ * console.log('Schedule:', schedule.name);
518
+ * console.log('Next run:', schedule.due_date);
519
+ * console.log('Destinations:', destinations.length);
520
+ * ```
521
+ */
522
+ async get(scheduleId: string): Promise<ScheduleGetResult> {
523
+ const url = buildUrl(this.#baseUrl, `/schedule/get/${encodeURIComponent(scheduleId)}`);
524
+ const signal = AbortSignal.timeout(30_000);
525
+ const res = await this.#adapter.invoke<ScheduleGetResult>(url, {
526
+ method: 'GET',
527
+ signal,
528
+ telemetry: {
529
+ name: 'agentuity.schedule.get',
530
+ attributes: {
531
+ scheduleId,
532
+ },
533
+ },
534
+ });
535
+
536
+ if (res.ok) {
537
+ return res.data;
538
+ }
539
+
540
+ if (res.response.status === 404) {
541
+ throw await toServiceException('GET', url, res.response);
542
+ }
543
+
544
+ throw await toServiceException('GET', url, res.response);
545
+ }
546
+
547
+ /**
548
+ * Update an existing schedule. Only the provided fields are modified.
549
+ *
550
+ * @remarks If the `expression` field is changed, the `due_date` is automatically
551
+ * recomputed based on the new cron expression.
552
+ *
553
+ * @param scheduleId - The schedule ID (prefixed with `sch_`)
554
+ * @param params - The fields to update (all optional)
555
+ * @returns The updated schedule record
556
+ * @throws ServiceException on API errors (e.g., invalid cron expression, schedule not found)
557
+ *
558
+ * @example
559
+ * ```typescript
560
+ * // Update the name and expression
561
+ * const { schedule } = await service.update('sch_abc123', {
562
+ * name: 'Daily Midnight Sync',
563
+ * expression: '0 0 * * *',
564
+ * });
565
+ * console.log('Updated. Next run:', schedule.due_date);
566
+ * ```
567
+ */
568
+ async update(scheduleId: string, params: UpdateScheduleParams): Promise<{ schedule: Schedule }> {
569
+ const url = buildUrl(this.#baseUrl, `/schedule/update/${encodeURIComponent(scheduleId)}`);
570
+ const signal = AbortSignal.timeout(30_000);
571
+ const res = await this.#adapter.invoke<{ schedule: Schedule }>(url, {
572
+ method: 'PUT',
573
+ signal,
574
+ body: JSON.stringify(params),
575
+ contentType: 'application/json',
576
+ telemetry: {
577
+ name: 'agentuity.schedule.update',
578
+ attributes: {
579
+ scheduleId,
580
+ },
581
+ },
582
+ });
583
+
584
+ if (res.ok) {
585
+ return res.data;
586
+ }
587
+
588
+ throw await toServiceException('PUT', url, res.response);
589
+ }
590
+
591
+ /**
592
+ * Delete a schedule and all of its destinations and delivery history.
593
+ *
594
+ * @param scheduleId - The schedule ID (prefixed with `sch_`)
595
+ * @throws ServiceException on API errors (e.g., schedule not found)
596
+ *
597
+ * @example
598
+ * ```typescript
599
+ * await service.delete('sch_abc123');
600
+ * console.log('Schedule deleted');
601
+ * ```
602
+ */
603
+ async delete(scheduleId: string): Promise<void> {
604
+ const url = buildUrl(this.#baseUrl, `/schedule/delete/${encodeURIComponent(scheduleId)}`);
605
+ const signal = AbortSignal.timeout(30_000);
606
+ const res = await this.#adapter.invoke<void>(url, {
607
+ method: 'DELETE',
608
+ signal,
609
+ telemetry: {
610
+ name: 'agentuity.schedule.delete',
611
+ attributes: {
612
+ scheduleId,
613
+ },
614
+ },
615
+ });
616
+
617
+ if (res.ok) {
618
+ return;
619
+ }
620
+
621
+ throw await toServiceException('DELETE', url, res.response);
622
+ }
623
+
624
+ /**
625
+ * Create a new destination on an existing schedule.
626
+ *
627
+ * @param scheduleId - The schedule ID (prefixed with `sch_`)
628
+ * @param params - The destination configuration including type and type-specific config
629
+ * @returns The newly created destination record
630
+ * @throws ServiceException on API errors (e.g., invalid URL, schedule not found)
631
+ *
632
+ * @example
633
+ * ```typescript
634
+ * const { destination } = await service.createDestination('sch_abc123', {
635
+ * type: 'url',
636
+ * config: {
637
+ * url: 'https://example.com/webhook',
638
+ * headers: { 'Authorization': 'Bearer token' },
639
+ * },
640
+ * });
641
+ * console.log('Destination created:', destination.id);
642
+ * ```
643
+ */
644
+ async createDestination(
645
+ scheduleId: string,
646
+ params: CreateScheduleDestinationParams
647
+ ): Promise<{ destination: ScheduleDestination }> {
648
+ const url = buildUrl(
649
+ this.#baseUrl,
650
+ `/schedule/destinations/create/${encodeURIComponent(scheduleId)}`
651
+ );
652
+ const signal = AbortSignal.timeout(30_000);
653
+ const res = await this.#adapter.invoke<{ destination: ScheduleDestination }>(url, {
654
+ method: 'POST',
655
+ signal,
656
+ body: JSON.stringify(params),
657
+ contentType: 'application/json',
658
+ telemetry: {
659
+ name: 'agentuity.schedule.createDestination',
660
+ attributes: {
661
+ scheduleId,
662
+ type: params.type,
663
+ },
664
+ },
665
+ });
666
+
667
+ if (res.ok) {
668
+ return res.data;
669
+ }
670
+
671
+ throw await toServiceException('POST', url, res.response);
672
+ }
673
+
674
+ /**
675
+ * Delete a destination from a schedule.
676
+ *
677
+ * @param destinationId - The destination ID (prefixed with `sdst_`)
678
+ * @throws ServiceException on API errors (e.g., destination not found)
679
+ *
680
+ * @example
681
+ * ```typescript
682
+ * await service.deleteDestination('sdst_xyz789');
683
+ * console.log('Destination removed');
684
+ * ```
685
+ */
686
+ async deleteDestination(destinationId: string): Promise<void> {
687
+ const url = buildUrl(
688
+ this.#baseUrl,
689
+ `/schedule/destinations/delete/${encodeURIComponent(destinationId)}`
690
+ );
691
+ const signal = AbortSignal.timeout(30_000);
692
+ const res = await this.#adapter.invoke<void>(url, {
693
+ method: 'DELETE',
694
+ signal,
695
+ telemetry: {
696
+ name: 'agentuity.schedule.deleteDestination',
697
+ attributes: {
698
+ destinationId,
699
+ },
700
+ },
701
+ });
702
+
703
+ if (res.ok) {
704
+ return;
705
+ }
706
+
707
+ throw await toServiceException('DELETE', url, res.response);
708
+ }
709
+
710
+ /**
711
+ * List delivery attempts for a schedule with optional pagination.
712
+ *
713
+ * @param scheduleId - The schedule ID (prefixed with `sch_`)
714
+ * @param params - Optional pagination parameters
715
+ * @param params.limit - Maximum number of deliveries to return
716
+ * @param params.offset - Number of deliveries to skip for pagination
717
+ * @returns A list of delivery attempt records
718
+ * @throws ServiceException on API errors (including 404 if schedule not found)
719
+ *
720
+ * @example
721
+ * ```typescript
722
+ * const { deliveries } = await service.listDeliveries('sch_abc123');
723
+ * for (const d of deliveries) {
724
+ * console.log(`${d.date}: ${d.status} (retries: ${d.retries})`);
725
+ * if (d.error) {
726
+ * console.error(' Error:', d.error);
727
+ * }
728
+ * }
729
+ * ```
730
+ */
731
+ async listDeliveries(
732
+ scheduleId: string,
733
+ params?: { limit?: number; offset?: number }
734
+ ): Promise<ScheduleDeliveryListResult> {
735
+ const qs = new URLSearchParams();
736
+ if (params?.limit !== undefined) {
737
+ qs.set('limit', String(params.limit));
738
+ }
739
+ if (params?.offset !== undefined) {
740
+ qs.set('offset', String(params.offset));
741
+ }
742
+
743
+ const basePath = `/schedule/deliveries/${encodeURIComponent(scheduleId)}`;
744
+ const path = qs.toString() ? `${basePath}?${qs.toString()}` : basePath;
745
+ const url = buildUrl(this.#baseUrl, path);
746
+ const signal = AbortSignal.timeout(30_000);
747
+ const res = await this.#adapter.invoke<ScheduleDeliveryListResult>(url, {
748
+ method: 'GET',
749
+ signal,
750
+ telemetry: {
751
+ name: 'agentuity.schedule.listDeliveries',
752
+ attributes: {
753
+ scheduleId,
754
+ limit: String(params?.limit ?? ''),
755
+ offset: String(params?.offset ?? ''),
756
+ },
757
+ },
758
+ });
759
+
760
+ if (res.ok) {
761
+ return res.data;
762
+ }
763
+
764
+ if (res.response.status === 404) {
765
+ throw await toServiceException('GET', url, res.response);
766
+ }
767
+
768
+ throw await toServiceException('GET', url, res.response);
769
+ }
770
+
771
+ /**
772
+ * Get a specific destination for a schedule by its ID.
773
+ *
774
+ * @remarks This is a convenience method that fetches the schedule and filters
775
+ * its destinations client-side. For large destination lists, prefer using
776
+ * {@link get} directly.
777
+ *
778
+ * @param scheduleId - The schedule ID (prefixed with `sch_`)
779
+ * @param destinationId - The destination ID (prefixed with `sdst_`)
780
+ * @returns The matching destination record
781
+ * @throws Error if the destination is not found within the schedule
782
+ * @throws ServiceException on API errors when fetching the schedule
783
+ *
784
+ * @example
785
+ * ```typescript
786
+ * const dest = await service.getDestination('sch_abc123', 'sdst_xyz789');
787
+ * console.log('Destination type:', dest.type);
788
+ * console.log('Config:', dest.config);
789
+ * ```
790
+ */
791
+ async getDestination(scheduleId: string, destinationId: string): Promise<ScheduleDestination> {
792
+ const result = await this.get(scheduleId);
793
+ const destination = result.destinations.find((d) => d.id === destinationId);
794
+ if (!destination) {
795
+ throw new Error(`Destination not found: ${destinationId}`);
796
+ }
797
+ return destination;
798
+ }
799
+
800
+ /**
801
+ * Get a specific delivery record by its ID.
802
+ *
803
+ * @remarks This is a convenience method that lists deliveries and filters
804
+ * client-side. Use the optional `params` to control the search window if
805
+ * the delivery may not be in the first page of results.
806
+ *
807
+ * @param scheduleId - The schedule ID (prefixed with `sch_`)
808
+ * @param deliveryId - The delivery ID to find
809
+ * @param params - Optional pagination parameters to control the search window
810
+ * @returns The matching delivery record
811
+ * @throws Error if the delivery is not found in the result set
812
+ * @throws ServiceException on API errors when listing deliveries
813
+ *
814
+ * @example
815
+ * ```typescript
816
+ * const delivery = await service.getDelivery('sch_abc123', 'del_xyz789');
817
+ * console.log('Status:', delivery.status);
818
+ * console.log('Retries:', delivery.retries);
819
+ * ```
820
+ */
821
+ async getDelivery(
822
+ scheduleId: string,
823
+ deliveryId: string,
824
+ params?: { limit?: number; offset?: number }
825
+ ): Promise<ScheduleDelivery> {
826
+ const result = await this.listDeliveries(scheduleId, params);
827
+ const delivery = result.deliveries.find((d) => d.id === deliveryId);
828
+ if (!delivery) {
829
+ throw new Error(`Delivery not found: ${deliveryId}`);
830
+ }
831
+ return delivery;
832
+ }
833
+ }