@xentom/integration-framework 0.0.0 → 0.0.2

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/CLAUDE.md CHANGED
@@ -1,836 +1,837 @@
1
- # CLAUDE.md
2
-
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
- ## Integration Development Commands
6
-
7
- ```bash
8
- # Build and test your integration
9
- npm run build
10
- npm run typecheck
11
- npm run lint
12
- ```
13
-
14
- ## Framework Overview
15
-
16
- This is the `@xentom/integration-framework` package for building workflow integrations. It provides a declarative,
17
- type-safe API for creating integrations that can process data through interconnected nodes.
18
-
19
- **Core Philosophy:**
20
-
21
- - **Type Safety**: Heavy use of TypeScript generics and inference
22
- - **Declarative**: Define what you want, not how to achieve it
23
- - **Composable**: Build complex workflows from simple, reusable components
24
- - **Standard Schema**: Compatible with any validation library using the Standard Schema spec
25
-
26
- Import the framework as:
27
-
28
- ```typescript
29
- import * as i from '@xentom/integration-framework';
30
- ```
31
-
32
- ## Integration Architecture
33
-
34
- ### Integration Structure
35
-
36
- Every integration must follow this structure:
37
-
38
- ```typescript
39
- export default i.integration({
40
- // Environment variables - secure configuration
41
- env: {
42
- API_KEY: i.env({
43
- control: i.controls.text({
44
- label: 'API Key',
45
- description: 'Your service API key for authentication',
46
- sensitive: true, // Hides value in UI
47
- }),
48
- }),
49
- },
50
-
51
- // Workflow nodes - the building blocks
52
- nodes: {
53
- // Your trigger, callable, and pure nodes
54
- },
55
-
56
- // Optional lifecycle hooks
57
- start({ state, webhook }) {
58
- // Initialize shared resources (API clients, connections, etc.)
59
- // This runs when the integration starts
60
- },
61
-
62
- stop({ state }) {
63
- // Clean up resources (close connections, clear timers, etc.)
64
- // This runs when the integration stops
65
- },
66
- });
67
- ```
68
-
69
- ### Integration State
70
-
71
- The integration provides an in-memory state object (`IntegrationState`) that is shared across all nodes:
72
-
73
- - **Purpose**: Store shared resources like API clients, caches, or connections
74
- - **Scope**: Available to all nodes and lifecycle hooks
75
- - **Lifecycle**: Exists only during integration runtime (not persisted)
76
- - **Usage**: Access via `state` parameter in node functions
77
-
78
- ## Node Types Deep Dive
79
-
80
- ### Trigger Nodes - Workflow Entry Points
81
-
82
- Trigger nodes are the **only** way to start a workflow. They listen for events and emit outputs when triggered.
83
-
84
- **Key Characteristics:**
85
-
86
- - Cannot be invoked by other nodes
87
- - Can invoke other nodes via their outputs
88
- - Must implement a `subscribe` function
89
- - Should return cleanup functions
90
-
91
- ```typescript
92
- webhookTrigger: i.nodes.trigger({
93
- // Optional: categorize your node in the UI
94
- category: { path: ['External', 'HTTP'] },
95
-
96
- // Optional: custom display name (defaults to key name in title case)
97
- displayName: 'Webhook Receiver',
98
-
99
- // Optional: description for UI and AI assistance
100
- description: 'Receives HTTP requests and processes the payload',
101
-
102
- // Inputs: configuration from user (not runtime data)
103
- inputs: {
104
- path: i.pins.data({
105
- control: i.controls.text({
106
- label: 'Webhook Path',
107
- placeholder: '/webhook',
108
- defaultValue: '/webhook',
109
- }),
110
- }),
111
- },
112
-
113
- // Outputs: data emitted when triggered
114
- outputs: {
115
- payload: i.pins.data({
116
- displayName: 'Request Payload',
117
- description: 'The parsed request body',
118
- }),
119
- headers: i.pins.data({
120
- displayName: 'HTTP Headers',
121
- description: 'Request headers as key-value pairs',
122
- }),
123
- },
124
-
125
- // Subscribe function: sets up event listeners
126
- subscribe({ next, webhook, inputs, state, variables }) {
127
- // Register webhook handler
128
- const unsubscribe = webhook.subscribe(async (req) => {
129
- try {
130
- const payload = await req.json();
131
-
132
- // Emit outputs and start workflow
133
- next({
134
- payload,
135
- headers: Object.fromEntries(req.headers),
136
- });
137
-
138
- // Return HTTP response
139
- return new Response('OK', { status: 200 });
140
- } catch (error) {
141
- return new Response('Bad Request', { status: 400 });
142
- }
143
- });
144
-
145
- // Always return cleanup function
146
- return () => unsubscribe();
147
- },
148
- }),
149
-
150
- // Timer trigger example
151
- timerTrigger: i.nodes.trigger({
152
- inputs: {
153
- interval: i.pins.data({
154
- control: i.controls.text({
155
- label: 'Interval (seconds)',
156
- defaultValue: '60',
157
- }),
158
- }),
159
- },
160
- outputs: {
161
- timestamp: i.pins.data(),
162
- },
163
- subscribe({ next, inputs }) {
164
- const intervalMs = parseInt(inputs.interval) * 1000;
165
-
166
- const timer = setInterval(() => {
167
- next({ timestamp: new Date().toISOString() });
168
- }, intervalMs);
169
-
170
- return () => clearInterval(timer);
171
- },
172
- }),
173
- ```
174
-
175
- ### Callable Nodes - Processing Units
176
-
177
- Callable nodes perform operations with side effects and explicitly control workflow execution.
178
-
179
- **Key Characteristics:**
180
-
181
- - Can be invoked by other nodes
182
- - Can invoke other nodes via exec pins
183
- - Must call `next()` to continue execution
184
- - Use `next()` to pass outputs
185
-
186
- ```typescript
187
- apiCall: i.nodes.callable({
188
- category: { path: ['API', 'HTTP'] },
189
- displayName: 'Make API Call',
190
- description: 'Performs HTTP requests to external APIs',
191
-
192
- inputs: {
193
- url: i.pins.data({
194
- control: i.controls.text({
195
- label: 'API Endpoint',
196
- placeholder: 'https://api.example.com/data',
197
- }),
198
- }),
199
- method: i.pins.data({
200
- control: i.controls.select({
201
- options: [
202
- { value: 'GET', label: 'GET' },
203
- { value: 'POST', label: 'POST' },
204
- { value: 'PUT', label: 'PUT' },
205
- { value: 'DELETE', label: 'DELETE' },
206
- ],
207
- placeholder: 'Select HTTP method',
208
- }),
209
- }),
210
- headers: i.pins.data({
211
- control: i.controls.expression({
212
- defaultValue: { 'Content-Type': 'application/json' },
213
- }),
214
- optional: true, // Pin won't show initially but can be added
215
- }),
216
- body: i.pins.data({
217
- control: i.controls.text({
218
- rows: 4, // Multi-line text area
219
- language: 'json', // Syntax highlighting
220
- }),
221
- optional: true,
222
- }),
223
- },
224
-
225
- outputs: {
226
- data: i.pins.data({
227
- displayName: 'Response Data',
228
- description: 'The parsed response body',
229
- }),
230
- status: i.pins.data({
231
- displayName: 'HTTP Status',
232
- description: 'The HTTP status code',
233
- }),
234
- headers: i.pins.data({
235
- displayName: 'Response Headers',
236
- description: 'Response headers as key-value pairs',
237
- }),
238
- },
239
-
240
- async run({ inputs, next, state, ctx, variables, webhook }) {
241
- // Access shared state (API client, etc.)
242
- const client = state.httpClient;
243
-
244
- // Perform the API call
245
- const response = await client.fetch(inputs.url, {
246
- method: inputs.method,
247
- headers: inputs.headers,
248
- body: inputs.body ? JSON.stringify(inputs.body) : undefined,
249
- });
250
-
251
- // Parse response
252
- const data = await response.json();
253
-
254
- // Pass outputs via next() - this continues the workflow
255
- next({
256
- data,
257
- status: response.status,
258
- headers: Object.fromEntries(response.headers),
259
- });
260
- },
261
- }),
262
- ```
263
-
264
- ### Pure Nodes - Computational Units
265
-
266
- Pure nodes are side-effect-free and compute outputs solely from inputs.
267
-
268
- **Key Characteristics:**
269
-
270
- - Cannot be invoked directly (only via data dependencies)
271
- - Cannot invoke other nodes
272
- - Automatically evaluated when their outputs are needed
273
- - Assign directly to `outputs` object
274
-
275
- ```typescript
276
- dataTransform: i.nodes.pure({
277
- category: { path: ['Data', 'Transform'] },
278
- displayName: 'Transform Data',
279
- description: 'Transforms input data using a specified mapping',
280
-
281
- inputs: {
282
- data: i.pins.data({
283
- control: i.controls.expression({
284
- placeholder: 'Enter data to transform',
285
- }),
286
- examples: [
287
- {
288
- title: 'Simple Object',
289
- value: { name: 'John', age: 30 },
290
- },
291
- {
292
- title: 'Array of Objects',
293
- value: [
294
- { id: 1, name: 'Alice' },
295
- { id: 2, name: 'Bob' },
296
- ],
297
- },
298
- ],
299
- }),
300
- mapping: i.pins.data({
301
- control: i.controls.expression({
302
- defaultValue: {
303
- newName: 'data.name',
304
- ageInMonths: 'data.age * 12',
305
- },
306
- }),
307
- }),
308
- },
309
-
310
- outputs: {
311
- result: i.pins.data({
312
- displayName: 'Transformed Data',
313
- description: 'The data after applying the mapping',
314
- }),
315
- },
316
-
317
- run({ inputs, outputs, state, ctx, variables, webhook }) {
318
- // Pure computation - no side effects
319
- const { data, mapping } = inputs;
320
-
321
- // Apply transformation
322
- const result = applyMapping(data, mapping);
323
-
324
- // Assign to outputs - no next() call needed
325
- outputs.result = result;
326
- },
327
- }),
328
- ```
329
-
330
- ## Pin System Deep Dive
331
-
332
- ### Data Pins - Information Flow
333
-
334
- Data pins handle the flow of information between nodes.
335
-
336
- ```typescript
337
- // Basic data pin
338
- i.pins.data()
339
-
340
- // Fully configured data pin
341
- i.pins.data({
342
- // UI Configuration
343
- displayName: 'User Input', // Custom label (default: key name)
344
- description: 'The user-provided input value',
345
-
346
- // Control for user input
347
- control: i.controls.text({
348
- label: 'Enter Value',
349
- placeholder: 'Type here...',
350
- defaultValue: 'Default text',
351
- }),
352
-
353
- // Schema validation (Standard Schema compatible)
354
- schema: v.pipe(v.string(), v.minLength(1), v.maxLength(100)),
355
-
356
- // Examples for users and AI
357
- examples: [
358
- { title: 'Simple Text', value: 'Hello World' },
359
- { title: 'Template', value: '{{variable}}' },
360
- ],
361
-
362
- // Optional pins don't show initially
363
- optional: true,
364
- }),
365
-
366
- // Method chaining with .with()
367
- i.pins.data().with({
368
- displayName: 'Custom Label',
369
- description: 'Additional configuration',
370
- }),
371
- ```
372
-
373
- ### Exec Pins - Execution Flow
374
-
375
- Exec pins control the execution flow in trigger and callable nodes.
376
-
377
- **Critical Rule: Only use exec pins for:**
378
-
379
- 1. **Branching Logic**: Conditional execution paths
380
- 2. **Iteration**: Processing arrays/collections
381
- 3. **State Machines**: Complex state transitions
382
-
383
- ```typescript
384
- // Branching example
385
- conditionalProcessor: i.nodes.callable({
386
- inputs: {
387
- condition: i.pins.data(),
388
- trueValue: i.pins.data(),
389
- falseValue: i.pins.data(),
390
- },
391
- outputs: {
392
- // Exec pins for different paths
393
- whenTrue: i.pins.exec({
394
- outputs: {
395
- value: i.pins.data(),
396
- },
397
- }),
398
- whenFalse: i.pins.exec({
399
- outputs: {
400
- value: i.pins.data(),
401
- },
402
- }),
403
- },
404
- run({ inputs, next }) {
405
- if (inputs.condition) {
406
- next('whenTrue', { value: inputs.trueValue });
407
- } else {
408
- next('whenFalse', { value: inputs.falseValue });
409
- }
410
- },
411
- }),
412
-
413
- // Iteration example
414
- arrayProcessor: i.nodes.callable({
415
- inputs: {
416
- items: i.pins.data(),
417
- },
418
- outputs: {
419
- // Exec pin for each iteration
420
- forEach: i.pins.exec({
421
- outputs: {
422
- item: i.pins.data(),
423
- index: i.pins.data(),
424
- },
425
- }),
426
- // Exec pin when all items processed
427
- completed: i.pins.exec({
428
- outputs: {
429
- count: i.pins.data(),
430
- },
431
- }),
432
- },
433
- run({ inputs, next }) {
434
- const items = inputs.items;
435
-
436
- // Process each item
437
- items.forEach((item, index) => {
438
- next('forEach', { item, index });
439
- });
440
-
441
- // Signal completion
442
- next('completed', { count: items.length });
443
- },
444
- }),
445
- ```
446
-
447
- ## Control System Deep Dive
448
-
449
- ### Text Controls - String Input
450
-
451
- ```typescript
452
- i.controls.text({
453
- // Base properties
454
- label: 'Input Label',
455
- description: 'Help text for the user',
456
- defaultValue: 'Default text',
457
-
458
- // Text-specific properties
459
- placeholder: 'Enter text here...',
460
- sensitive: true, // Hides input value (passwords, API keys)
461
- rows: 4, // Multi-line text area
462
- language: 'json', // Syntax highlighting: 'plain', 'html', 'markdown'
463
- })
464
- ```
465
-
466
- ### Expression Controls - JavaScript Code
467
-
468
- ```typescript
469
- i.controls.expression({
470
- // Base properties
471
- label: 'Expression',
472
- description: 'JavaScript expression to evaluate',
473
- defaultValue: { result: 'computed value' },
474
-
475
- // Expression-specific properties
476
- placeholder: 'Enter JavaScript expression...',
477
- rows: 6, // Multi-line code editor
478
- })
479
- ```
480
-
481
- ### Select Controls - Dropdown Selection
482
-
483
- ```typescript
484
- // Static options
485
- i.controls.select({
486
- label: 'Choose Option',
487
- placeholder: 'Select an option...',
488
- options: [
489
- {
490
- value: 'option1',
491
- label: 'Option 1',
492
- description: 'Description of option 1'
493
- },
494
- {
495
- value: 'option2',
496
- label: 'Option 2',
497
- description: 'Description of option 2'
498
- },
499
- ],
500
- }),
501
-
502
- // Dynamic options (only for node pins, not env variables)
503
- i.controls.select({
504
- label: 'API Endpoint',
505
- placeholder: 'Select endpoint...',
506
- options: async ({ state }) => {
507
- // Access shared state to fetch options
508
- const endpoints = await state.apiClient.getEndpoints();
509
- return endpoints.map(ep => ({
510
- value: ep.url,
511
- label: ep.name,
512
- description: ep.description,
513
- }));
514
- },
515
- }),
516
- ```
517
-
518
- ### Switch Controls - Boolean Toggle
519
-
520
- ```typescript
521
- i.controls.switch({
522
- label: 'Enable Feature',
523
- description: 'Toggle this feature on or off',
524
- defaultValue: false,
525
- })
526
- ```
527
-
528
- ## Environment Variables
529
-
530
- Environment variables are secure configuration values that are set once and used across all nodes.
531
-
532
- ```typescript
533
- export default i.integration({
534
- env: {
535
- API_KEY: i.env({
536
- control: i.controls.text({
537
- label: 'API Key',
538
- description: 'Your service API key for authentication',
539
- placeholder: 'sk-...',
540
- sensitive: true, // Important: hides the value
541
- }),
542
- // Optional: validation schema
543
- schema: v.pipe(v.string(), v.startsWith('sk-')),
544
- }),
545
-
546
- DEBUG_MODE: i.env({
547
- control: i.controls.switch({
548
- label: 'Debug Mode',
549
- description: 'Enable debug logging',
550
- defaultValue: false,
551
- }),
552
- }),
553
-
554
- REGION: i.env({
555
- control: i.controls.select({
556
- options: [
557
- { value: 'us-east-1', label: 'US East 1' },
558
- { value: 'us-west-2', label: 'US West 2' },
559
- { value: 'eu-west-1', label: 'EU West 1' },
560
- ],
561
- }),
562
- }),
563
- },
564
-
565
- // Environment variables are available in start/stop hooks
566
- async start({ state, env }) {
567
- // Use environment variables to initialize shared resources
568
- state.apiClient = new ApiClient({
569
- apiKey: env.API_KEY,
570
- region: env.REGION,
571
- debug: env.DEBUG_MODE,
572
- });
573
- },
574
-
575
- nodes: {
576
- // Environment variables are NOT directly available in nodes
577
- // Access them through shared state or pass as inputs
578
- },
579
- });
580
- ```
581
-
582
- ## Error Handling
583
-
584
- **Golden Rule: Always throw errors, never try to handle them with exec pins or return values.**
585
-
586
- ```typescript
587
- // Correct error handling in callable nodes
588
- async run({ inputs, next, state }) {
589
- try {
590
- const response = await state.apiClient.get(inputs.url);
591
-
592
- // Check for API errors
593
- if (!response.ok) {
594
- throw new Error(`API request failed: ${response.status} ${response.statusText}`);
595
- }
596
-
597
- const data = await response.json();
598
- next({ data });
599
- } catch (error) {
600
- // Let the error bubble up - the framework will handle it
601
- throw error;
602
- }
603
- }
604
-
605
- // Correct error handling in pure nodes
606
- run({ inputs, outputs }) {
607
- if (!inputs.value) {
608
- throw new Error('Value is required');
609
- }
610
-
611
- if (typeof inputs.value !== 'string') {
612
- throw new Error('Value must be a string');
613
- }
614
-
615
- outputs.result = inputs.value.toUpperCase();
616
- }
617
-
618
- // Correct error handling in triggers
619
- subscribe({ next, webhook, state }) {
620
- const unsubscribe = webhook.subscribe(async (req) => {
621
- try {
622
- const payload = await req.json();
623
- next({ payload });
624
- return new Response('OK');
625
- } catch (error) {
626
- // Handle webhook-specific errors
627
- console.error('Webhook error:', error);
628
- return new Response('Bad Request', { status: 400 });
629
- }
630
- });
631
-
632
- return () => unsubscribe();
633
- }
634
- ```
635
-
636
- ## Advanced Patterns
637
-
638
- ### State Management with Lifecycle Hooks
639
-
640
- ```typescript
641
- export default i.integration({
642
- env: {
643
- DATABASE_URL: i.env({
644
- control: i.controls.text({ sensitive: true }),
645
- }),
646
- },
647
-
648
- async start({ state, env }) {
649
- // Initialize shared resources
650
- state.db = new Database(env.DATABASE_URL);
651
- state.cache = new Map();
652
-
653
- // Setup connections
654
- await state.db.connect();
655
-
656
- // Initialize other services
657
- state.emailService = new EmailService();
658
- },
659
-
660
- async stop({ state }) {
661
- // Clean up resources
662
- if (state.db) {
663
- await state.db.disconnect();
664
- }
665
-
666
- if (state.cache) {
667
- state.cache.clear();
668
- }
669
- },
670
-
671
- nodes: {
672
- // Nodes can access shared state
673
- dbQuery: i.nodes.callable({
674
- inputs: {
675
- query: i.pins.data(),
676
- },
677
- outputs: {
678
- result: i.pins.data(),
679
- },
680
- async run({ inputs, next, state }) {
681
- const result = await state.db.query(inputs.query);
682
- next({ result });
683
- },
684
- }),
685
- },
686
- });
687
- ```
688
-
689
- ### Complex Webhook Handling
690
-
691
- ```typescript
692
- webhookProcessor: i.nodes.trigger({
693
- inputs: {
694
- secretKey: i.pins.data({
695
- control: i.controls.text({
696
- label: 'Webhook Secret',
697
- sensitive: true,
698
- }),
699
- }),
700
- },
701
- outputs: {
702
- verified: i.pins.exec({
703
- outputs: {
704
- payload: i.pins.data(),
705
- signature: i.pins.data(),
706
- },
707
- }),
708
- invalid: i.pins.exec(),
709
- },
710
-
711
- subscribe({ next, webhook, inputs }) {
712
- const unsubscribe = webhook.subscribe(async (req) => {
713
- try {
714
- // Verify webhook signature
715
- const signature = req.headers.get('X-Signature');
716
- const payload = await req.text();
717
-
718
- if (!verifySignature(payload, signature, inputs.secretKey)) {
719
- next('invalid');
720
- return new Response('Unauthorized', { status: 401 });
721
- }
722
-
723
- // Process verified webhook
724
- const data = JSON.parse(payload);
725
- next('verified', { payload: data, signature });
726
-
727
- return new Response('OK');
728
- } catch (error) {
729
- next('invalid');
730
- return new Response('Bad Request', { status: 400 });
731
- }
732
- });
733
-
734
- return () => unsubscribe();
735
- },
736
- }),
737
- ```
738
-
739
- ### Dynamic Options with Caching
740
-
741
- ```typescript
742
- apiEndpointSelector: i.pins.data({
743
- control: i.controls.select({
744
- options: async ({ state }) => {
745
- // Check cache first
746
- if (state.endpointCache) {
747
- return state.endpointCache;
748
- }
749
-
750
- // Fetch from API
751
- const endpoints = await state.apiClient.getEndpoints();
752
- const options = endpoints.map(ep => ({
753
- value: ep.id,
754
- label: ep.name,
755
- description: `${ep.method} ${ep.path}`,
756
- }));
757
-
758
- // Cache results
759
- state.endpointCache = options;
760
-
761
- return options;
762
- },
763
- }),
764
- }),
765
- ```
766
-
767
- ## Type Safety and Inference
768
-
769
- The framework provides comprehensive TypeScript support:
770
-
771
- ```typescript
772
- // Type inference from integration definition
773
- const myIntegration = i.integration({
774
- nodes: {
775
- processor: i.nodes.callable({
776
- inputs: {
777
- data: i.pins.data(),
778
- },
779
- outputs: {
780
- result: i.pins.data(),
781
- },
782
- run({ inputs, next }) {
783
- // inputs.data is properly typed
784
- // next is properly typed
785
- next({ result: inputs.data });
786
- },
787
- }),
788
- },
789
- });
790
-
791
- // Extract types from integration
792
- type IntegrationOutput = typeof myIntegration.$infer;
793
- // IntegrationOutput.nodes.processor.inputs.data is typed
794
- // IntegrationOutput.nodes.processor.outputs.result is typed
795
- ```
796
-
797
- ## Development Best Practices
798
-
799
- 1. **Use TypeScript**: The framework is built for TypeScript - use it
800
- 2. **Descriptive Names**: Use clear, descriptive names for nodes, pins, and variables
801
- 3. **Categories**: Organize nodes with categories for better UX
802
- 4. **Documentation**: Add descriptions to nodes and pins for AI assistance
803
- 5. **Examples**: Provide examples for complex data pins
804
- 6. **State Management**: Use integration state for shared resources
805
- 7. **Error Handling**: Always throw errors, never handle them with exec pins
806
- 8. **Cleanup**: Always return cleanup functions from trigger subscriptions
807
- 9. **Optional Pins**: Use optional pins to reduce UI clutter
808
- 10. **Validation**: Use schema validation for robust data handling
809
-
810
- ## Testing Guidelines
811
-
812
- ```typescript
813
- // Test pure nodes easily
814
- import { describe, it, expect } from 'vitest';
815
- import { integration } from './my-integration';
816
-
817
- describe('Data Transform Node', () => {
818
- it('should transform data correctly', () => {
819
- const node = integration.nodes.dataTransform;
820
- const outputs = {};
821
-
822
- node.run({
823
- inputs: { data: { name: 'John' }, mapping: { title: 'data.name' } },
824
- outputs,
825
- state: {},
826
- ctx: {},
827
- variables: {},
828
- webhook: { url: 'http://test' },
829
- });
830
-
831
- expect(outputs.result).toEqual({ title: 'John' });
832
- });
833
- });
834
- ```
835
-
836
- This framework enables you to build powerful, type-safe integrations with clear separation of concerns and excellent developer experience.
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Integration Development Commands
6
+
7
+ ```bash
8
+ # Build and test your integration
9
+ npm run build
10
+ npm run typecheck
11
+ npm run lint
12
+ ```
13
+
14
+ ## Framework Overview
15
+
16
+ This is the `@xentom/integration-framework` package for building workflow integrations. It provides a declarative,
17
+ type-safe API for creating integrations that can process data through interconnected nodes.
18
+
19
+ **Core Philosophy:**
20
+
21
+ - **Type Safety**: Heavy use of TypeScript generics and inference
22
+ - **Declarative**: Define what you want, not how to achieve it
23
+ - **Composable**: Build complex workflows from simple, reusable components
24
+ - **Standard Schema**: Compatible with any validation library using the Standard Schema spec
25
+
26
+ Import the framework as:
27
+
28
+ ```typescript
29
+ import * as i from '@xentom/integration-framework';
30
+ ```
31
+
32
+ ## Integration Architecture
33
+
34
+ ### Integration Structure
35
+
36
+ Every integration must follow this structure:
37
+
38
+ ```typescript
39
+ export default i.integration({
40
+ // Environment variables - secure configuration
41
+ env: {
42
+ API_KEY: i.env({
43
+ control: i.controls.text({
44
+ label: 'API Key',
45
+ description: 'Your service API key for authentication',
46
+ sensitive: true, // Hides value in UI
47
+ }),
48
+ }),
49
+ },
50
+
51
+ // Workflow nodes - the building blocks
52
+ nodes: {
53
+ // Your trigger, callable, and pure nodes
54
+ },
55
+
56
+ // Optional lifecycle hooks
57
+ start({ state, webhook }) {
58
+ // Initialize shared resources (API clients, connections, etc.)
59
+ // This runs when the integration starts
60
+ },
61
+
62
+ stop({ state }) {
63
+ // Clean up resources (close connections, clear timers, etc.)
64
+ // This runs when the integration stops
65
+ },
66
+ });
67
+ ```
68
+
69
+ ### Integration State
70
+
71
+ The integration provides an in-memory state object (`IntegrationState`) that is shared across all nodes:
72
+
73
+ - **Purpose**: Store shared resources like API clients, caches, or connections
74
+ - **Scope**: Available to all nodes and lifecycle hooks
75
+ - **Lifecycle**: Exists only during integration runtime (not persisted)
76
+ - **Usage**: Access via `state` parameter in node functions
77
+
78
+ ## Node Types Deep Dive
79
+
80
+ ### Trigger Nodes - Workflow Entry Points
81
+
82
+ Trigger nodes are the **only** way to start a workflow. They listen for events and emit outputs when triggered.
83
+
84
+ **Key Characteristics:**
85
+
86
+ - Cannot be invoked by other nodes
87
+ - Can invoke other nodes via their outputs
88
+ - Must implement a `subscribe` function
89
+ - Should return cleanup functions
90
+
91
+ ```typescript
92
+ webhookTrigger: i.nodes.trigger({
93
+ // Optional: categorize your node in the UI
94
+ category: { path: ['External', 'HTTP'] },
95
+
96
+ // Optional: custom display name (defaults to key name in title case)
97
+ displayName: 'Webhook Receiver',
98
+
99
+ // Optional: description for UI and AI assistance
100
+ description: 'Receives HTTP requests and processes the payload',
101
+
102
+ // Inputs: configuration from user (not runtime data)
103
+ inputs: {
104
+ path: i.pins.data({
105
+ control: i.controls.text({
106
+ label: 'Webhook Path',
107
+ placeholder: '/webhook',
108
+ defaultValue: '/webhook',
109
+ }),
110
+ }),
111
+ },
112
+
113
+ // Outputs: data emitted when triggered
114
+ outputs: {
115
+ payload: i.pins.data({
116
+ displayName: 'Request Payload',
117
+ description: 'The parsed request body',
118
+ }),
119
+ headers: i.pins.data({
120
+ displayName: 'HTTP Headers',
121
+ description: 'Request headers as key-value pairs',
122
+ }),
123
+ },
124
+
125
+ // Subscribe function: sets up event listeners
126
+ subscribe({ next, webhook, inputs, state, variables }) {
127
+ // Register webhook handler
128
+ const unsubscribe = webhook.subscribe(async (req) => {
129
+ try {
130
+ const payload = await req.json();
131
+
132
+ // Emit outputs and start workflow
133
+ next({
134
+ payload,
135
+ headers: Object.fromEntries(req.headers),
136
+ });
137
+
138
+ // Return HTTP response
139
+ return new Response('OK', { status: 200 });
140
+ } catch (error) {
141
+ return new Response('Bad Request', { status: 400 });
142
+ }
143
+ });
144
+
145
+ // Always return cleanup function
146
+ return () => unsubscribe();
147
+ },
148
+ }),
149
+
150
+ // Timer trigger example
151
+ timerTrigger: i.nodes.trigger({
152
+ inputs: {
153
+ interval: i.pins.data({
154
+ control: i.controls.text({
155
+ label: 'Interval (seconds)',
156
+ defaultValue: '60',
157
+ }),
158
+ }),
159
+ },
160
+ outputs: {
161
+ timestamp: i.pins.data(),
162
+ },
163
+ subscribe({ next, inputs }) {
164
+ const intervalMs = parseInt(inputs.interval) * 1000;
165
+
166
+ const timer = setInterval(() => {
167
+ next({ timestamp: new Date().toISOString() });
168
+ }, intervalMs);
169
+
170
+ return () => clearInterval(timer);
171
+ },
172
+ }),
173
+ ```
174
+
175
+ ### Callable Nodes - Processing Units
176
+
177
+ Callable nodes perform operations with side effects and explicitly control workflow execution.
178
+
179
+ **Key Characteristics:**
180
+
181
+ - Can be invoked by other nodes
182
+ - Can invoke other nodes via exec pins
183
+ - Must call `next()` to continue execution
184
+ - Use `next()` to pass outputs
185
+
186
+ ```typescript
187
+ apiCall: i.nodes.callable({
188
+ category: { path: ['API', 'HTTP'] },
189
+ displayName: 'Make API Call',
190
+ description: 'Performs HTTP requests to external APIs',
191
+
192
+ inputs: {
193
+ url: i.pins.data({
194
+ control: i.controls.text({
195
+ label: 'API Endpoint',
196
+ placeholder: 'https://api.example.com/data',
197
+ }),
198
+ }),
199
+ method: i.pins.data({
200
+ control: i.controls.select({
201
+ options: [
202
+ { value: 'GET', label: 'GET' },
203
+ { value: 'POST', label: 'POST' },
204
+ { value: 'PUT', label: 'PUT' },
205
+ { value: 'DELETE', label: 'DELETE' },
206
+ ],
207
+ placeholder: 'Select HTTP method',
208
+ }),
209
+ }),
210
+ headers: i.pins.data({
211
+ control: i.controls.expression({
212
+ defaultValue: { 'Content-Type': 'application/json' },
213
+ }),
214
+ optional: true, // Pin won't show initially but can be added
215
+ }),
216
+ body: i.pins.data({
217
+ control: i.controls.text({
218
+ rows: 4, // Multi-line text area
219
+ language: 'json', // Syntax highlighting
220
+ }),
221
+ optional: true,
222
+ }),
223
+ },
224
+
225
+ outputs: {
226
+ data: i.pins.data({
227
+ displayName: 'Response Data',
228
+ description: 'The parsed response body',
229
+ }),
230
+ status: i.pins.data({
231
+ displayName: 'HTTP Status',
232
+ description: 'The HTTP status code',
233
+ }),
234
+ headers: i.pins.data({
235
+ displayName: 'Response Headers',
236
+ description: 'Response headers as key-value pairs',
237
+ }),
238
+ },
239
+
240
+ async run({ inputs, next, state, ctx, variables, webhook }) {
241
+ // Access shared state (API client, etc.)
242
+ const client = state.httpClient;
243
+
244
+ // Perform the API call
245
+ const response = await client.fetch(inputs.url, {
246
+ method: inputs.method,
247
+ headers: inputs.headers,
248
+ body: inputs.body ? JSON.stringify(inputs.body) : undefined,
249
+ });
250
+
251
+ // Parse response
252
+ const data = await response.json();
253
+
254
+ // Pass outputs via next() - this continues the workflow
255
+ next({
256
+ data,
257
+ status: response.status,
258
+ headers: Object.fromEntries(response.headers),
259
+ });
260
+ },
261
+ }),
262
+ ```
263
+
264
+ ### Pure Nodes - Computational Units
265
+
266
+ Pure nodes are side-effect-free and compute outputs solely from inputs.
267
+
268
+ **Key Characteristics:**
269
+
270
+ - Cannot be invoked directly (only via data dependencies)
271
+ - Cannot invoke other nodes
272
+ - Automatically evaluated when their outputs are needed
273
+ - Assign directly to `outputs` object
274
+
275
+ ```typescript
276
+ dataTransform: i.nodes.pure({
277
+ category: { path: ['Data', 'Transform'] },
278
+ displayName: 'Transform Data',
279
+ description: 'Transforms input data using a specified mapping',
280
+
281
+ inputs: {
282
+ data: i.pins.data({
283
+ control: i.controls.expression({
284
+ placeholder: 'Enter data to transform',
285
+ }),
286
+ examples: [
287
+ {
288
+ title: 'Simple Object',
289
+ value: { name: 'John', age: 30 },
290
+ },
291
+ {
292
+ title: 'Array of Objects',
293
+ value: [
294
+ { id: 1, name: 'Alice' },
295
+ { id: 2, name: 'Bob' },
296
+ ],
297
+ },
298
+ ],
299
+ }),
300
+ mapping: i.pins.data({
301
+ control: i.controls.expression({
302
+ defaultValue: {
303
+ newName: 'data.name',
304
+ ageInMonths: 'data.age * 12',
305
+ },
306
+ }),
307
+ }),
308
+ },
309
+
310
+ outputs: {
311
+ result: i.pins.data({
312
+ displayName: 'Transformed Data',
313
+ description: 'The data after applying the mapping',
314
+ }),
315
+ },
316
+
317
+ run({ inputs, outputs, state, ctx, variables, webhook }) {
318
+ // Pure computation - no side effects
319
+ const { data, mapping } = inputs;
320
+
321
+ // Apply transformation
322
+ const result = applyMapping(data, mapping);
323
+
324
+ // Assign to outputs - no next() call needed
325
+ outputs.result = result;
326
+ },
327
+ }),
328
+ ```
329
+
330
+ ## Pin System Deep Dive
331
+
332
+ ### Data Pins - Information Flow
333
+
334
+ Data pins handle the flow of information between nodes.
335
+
336
+ ```typescript
337
+ // Basic data pin
338
+ i.pins.data()
339
+
340
+ // Fully configured data pin
341
+ i.pins.data({
342
+ // UI Configuration
343
+ displayName: 'User Input', // Custom label (default: key name)
344
+ description: 'The user-provided input value',
345
+
346
+ // Control for user input
347
+ control: i.controls.text({
348
+ label: 'Enter Value',
349
+ placeholder: 'Type here...',
350
+ defaultValue: 'Default text',
351
+ }),
352
+
353
+ // Schema validation (Standard Schema compatible)
354
+ schema: v.pipe(v.string(), v.minLength(1), v.maxLength(100)),
355
+
356
+ // Examples for users and AI
357
+ examples: [
358
+ { title: 'Simple Text', value: 'Hello World' },
359
+ { title: 'Template', value: '{{variable}}' },
360
+ ],
361
+
362
+ // Optional pins don't show initially
363
+ optional: true,
364
+ }),
365
+
366
+ // Method chaining with .with()
367
+ i.pins.data().with({
368
+ displayName: 'Custom Label',
369
+ description: 'Additional configuration',
370
+ }),
371
+ ```
372
+
373
+ ### Exec Pins - Execution Flow
374
+
375
+ Exec pins control the execution flow in trigger and callable nodes.
376
+
377
+ **Critical Rule: Only use exec pins for:**
378
+
379
+ 1. **Branching Logic**: Conditional execution paths
380
+ 2. **Iteration**: Processing arrays/collections
381
+ 3. **State Machines**: Complex state transitions
382
+
383
+ ```typescript
384
+ // Branching example
385
+ conditionalProcessor: i.nodes.callable({
386
+ inputs: {
387
+ condition: i.pins.data(),
388
+ trueValue: i.pins.data(),
389
+ falseValue: i.pins.data(),
390
+ },
391
+ outputs: {
392
+ // Exec pins for different paths
393
+ whenTrue: i.pins.exec({
394
+ outputs: {
395
+ value: i.pins.data(),
396
+ },
397
+ }),
398
+ whenFalse: i.pins.exec({
399
+ outputs: {
400
+ value: i.pins.data(),
401
+ },
402
+ }),
403
+ },
404
+ run({ inputs, next }) {
405
+ if (inputs.condition) {
406
+ next('whenTrue', { value: inputs.trueValue });
407
+ } else {
408
+ next('whenFalse', { value: inputs.falseValue });
409
+ }
410
+ },
411
+ }),
412
+
413
+ // Iteration example
414
+ arrayProcessor: i.nodes.callable({
415
+ inputs: {
416
+ items: i.pins.data(),
417
+ },
418
+ outputs: {
419
+ // Exec pin for each iteration
420
+ forEach: i.pins.exec({
421
+ outputs: {
422
+ item: i.pins.data(),
423
+ index: i.pins.data(),
424
+ },
425
+ }),
426
+ // Exec pin when all items processed
427
+ completed: i.pins.exec({
428
+ outputs: {
429
+ count: i.pins.data(),
430
+ },
431
+ }),
432
+ },
433
+ run({ inputs, next }) {
434
+ const items = inputs.items;
435
+
436
+ // Process each item
437
+ items.forEach((item, index) => {
438
+ next('forEach', { item, index });
439
+ });
440
+
441
+ // Signal completion
442
+ next('completed', { count: items.length });
443
+ },
444
+ }),
445
+ ```
446
+
447
+ ## Control System Deep Dive
448
+
449
+ ### Text Controls - String Input
450
+
451
+ ```typescript
452
+ i.controls.text({
453
+ // Base properties
454
+ label: 'Input Label',
455
+ description: 'Help text for the user',
456
+ defaultValue: 'Default text',
457
+
458
+ // Text-specific properties
459
+ placeholder: 'Enter text here...',
460
+ sensitive: true, // Hides input value (passwords, API keys)
461
+ rows: 4, // Multi-line text area
462
+ language: 'json', // Syntax highlighting: 'plain', 'html', 'markdown'
463
+ });
464
+ ```
465
+
466
+ ### Expression Controls - JavaScript Code
467
+
468
+ ```typescript
469
+ i.controls.expression({
470
+ // Base properties
471
+ label: 'Expression',
472
+ description: 'JavaScript expression to evaluate',
473
+ defaultValue: { result: 'computed value' },
474
+
475
+ // Expression-specific properties
476
+ placeholder: 'Enter JavaScript expression...',
477
+ rows: 6, // Multi-line code editor
478
+ });
479
+ ```
480
+
481
+ ### Select Controls - Dropdown Selection
482
+
483
+ ```typescript
484
+ // Static options
485
+ i.controls.select({
486
+ label: 'Choose Option',
487
+ placeholder: 'Select an option...',
488
+ options: [
489
+ {
490
+ value: 'option1',
491
+ label: 'Option 1',
492
+ description: 'Description of option 1'
493
+ },
494
+ {
495
+ value: 'option2',
496
+ label: 'Option 2',
497
+ description: 'Description of option 2'
498
+ },
499
+ ],
500
+ }),
501
+
502
+ // Dynamic options (only for node pins, not env variables)
503
+ i.controls.select({
504
+ label: 'API Endpoint',
505
+ placeholder: 'Select endpoint...',
506
+ options: async ({ state }) => {
507
+ // Access shared state to fetch options
508
+ const endpoints = await state.apiClient.getEndpoints();
509
+ return endpoints.map(ep => ({
510
+ value: ep.url,
511
+ label: ep.name,
512
+ description: ep.description,
513
+ }));
514
+ },
515
+ }),
516
+ ```
517
+
518
+ ### Switch Controls - Boolean Toggle
519
+
520
+ ```typescript
521
+ i.controls.switch({
522
+ label: 'Enable Feature',
523
+ description: 'Toggle this feature on or off',
524
+ defaultValue: false,
525
+ });
526
+ ```
527
+
528
+ ## Environment Variables
529
+
530
+ Environment variables are secure configuration values that are set once and used across all nodes.
531
+
532
+ ```typescript
533
+ export default i.integration({
534
+ env: {
535
+ API_KEY: i.env({
536
+ control: i.controls.text({
537
+ label: 'API Key',
538
+ description: 'Your service API key for authentication',
539
+ placeholder: 'sk-...',
540
+ sensitive: true, // Important: hides the value
541
+ }),
542
+ // Optional: validation schema
543
+ schema: v.pipe(v.string(), v.startsWith('sk-')),
544
+ }),
545
+
546
+ DEBUG_MODE: i.env({
547
+ control: i.controls.switch({
548
+ label: 'Debug Mode',
549
+ description: 'Enable debug logging',
550
+ defaultValue: false,
551
+ }),
552
+ }),
553
+
554
+ REGION: i.env({
555
+ control: i.controls.select({
556
+ options: [
557
+ { value: 'us-east-1', label: 'US East 1' },
558
+ { value: 'us-west-2', label: 'US West 2' },
559
+ { value: 'eu-west-1', label: 'EU West 1' },
560
+ ],
561
+ }),
562
+ }),
563
+ },
564
+
565
+ // Environment variables are available in start/stop hooks
566
+ async start({ state, env }) {
567
+ // Use environment variables to initialize shared resources
568
+ state.apiClient = new ApiClient({
569
+ apiKey: env.API_KEY,
570
+ region: env.REGION,
571
+ debug: env.DEBUG_MODE,
572
+ });
573
+ },
574
+
575
+ nodes: {
576
+ // Environment variables are NOT directly available in nodes
577
+ // Access them through shared state or pass as inputs
578
+ },
579
+ });
580
+ ```
581
+
582
+ ## Error Handling
583
+
584
+ **Golden Rule: Always throw errors, never try to handle them with exec pins or return values.**
585
+
586
+ ```typescript
587
+ // Correct error handling in callable nodes
588
+ async run({ inputs, next, state }) {
589
+ try {
590
+ const response = await state.apiClient.get(inputs.url);
591
+
592
+ // Check for API errors
593
+ if (!response.ok) {
594
+ throw new Error(`API request failed: ${response.status} ${response.statusText}`);
595
+ }
596
+
597
+ const data = await response.json();
598
+ next({ data });
599
+ } catch (error) {
600
+ // Let the error bubble up - the framework will handle it
601
+ throw error;
602
+ }
603
+ }
604
+
605
+ // Correct error handling in pure nodes
606
+ run({ inputs, outputs }) {
607
+ if (!inputs.value) {
608
+ throw new Error('Value is required');
609
+ }
610
+
611
+ if (typeof inputs.value !== 'string') {
612
+ throw new Error('Value must be a string');
613
+ }
614
+
615
+ outputs.result = inputs.value.toUpperCase();
616
+ }
617
+
618
+ // Correct error handling in triggers
619
+ subscribe({ next, webhook, state }) {
620
+ const unsubscribe = webhook.subscribe(async (req) => {
621
+ try {
622
+ const payload = await req.json();
623
+ next({ payload });
624
+ return new Response('OK');
625
+ } catch (error) {
626
+ // Handle webhook-specific errors
627
+ console.error('Webhook error:', error);
628
+ return new Response('Bad Request', { status: 400 });
629
+ }
630
+ });
631
+
632
+ return () => unsubscribe();
633
+ }
634
+ ```
635
+
636
+ ## Advanced Patterns
637
+
638
+ ### State Management with Lifecycle Hooks
639
+
640
+ ```typescript
641
+ export default i.integration({
642
+ env: {
643
+ DATABASE_URL: i.env({
644
+ control: i.controls.text({ sensitive: true }),
645
+ }),
646
+ },
647
+
648
+ async start({ state, env }) {
649
+ // Initialize shared resources
650
+ state.db = new Database(env.DATABASE_URL);
651
+ state.cache = new Map();
652
+
653
+ // Setup connections
654
+ await state.db.connect();
655
+
656
+ // Initialize other services
657
+ state.emailService = new EmailService();
658
+ },
659
+
660
+ async stop({ state }) {
661
+ // Clean up resources
662
+ if (state.db) {
663
+ await state.db.disconnect();
664
+ }
665
+
666
+ if (state.cache) {
667
+ state.cache.clear();
668
+ }
669
+ },
670
+
671
+ nodes: {
672
+ // Nodes can access shared state
673
+ dbQuery: i.nodes.callable({
674
+ inputs: {
675
+ query: i.pins.data(),
676
+ },
677
+ outputs: {
678
+ result: i.pins.data(),
679
+ },
680
+ async run({ inputs, next, state }) {
681
+ const result = await state.db.query(inputs.query);
682
+ next({ result });
683
+ },
684
+ }),
685
+ },
686
+ });
687
+ ```
688
+
689
+ ### Complex Webhook Handling
690
+
691
+ ```typescript
692
+ webhookProcessor: i.nodes.trigger({
693
+ inputs: {
694
+ secretKey: i.pins.data({
695
+ control: i.controls.text({
696
+ label: 'Webhook Secret',
697
+ sensitive: true,
698
+ }),
699
+ }),
700
+ },
701
+ outputs: {
702
+ verified: i.pins.exec({
703
+ outputs: {
704
+ payload: i.pins.data(),
705
+ signature: i.pins.data(),
706
+ },
707
+ }),
708
+ invalid: i.pins.exec(),
709
+ },
710
+
711
+ subscribe({ next, webhook, inputs }) {
712
+ const unsubscribe = webhook.subscribe(async (req) => {
713
+ try {
714
+ // Verify webhook signature
715
+ const signature = req.headers.get('X-Signature');
716
+ const payload = await req.text();
717
+
718
+ if (!verifySignature(payload, signature, inputs.secretKey)) {
719
+ next('invalid');
720
+ return new Response('Unauthorized', { status: 401 });
721
+ }
722
+
723
+ // Process verified webhook
724
+ const data = JSON.parse(payload);
725
+ next('verified', { payload: data, signature });
726
+
727
+ return new Response('OK');
728
+ } catch (error) {
729
+ next('invalid');
730
+ return new Response('Bad Request', { status: 400 });
731
+ }
732
+ });
733
+
734
+ return () => unsubscribe();
735
+ },
736
+ }),
737
+ ```
738
+
739
+ ### Dynamic Options with Caching
740
+
741
+ ```typescript
742
+ apiEndpointSelector: i.pins.data({
743
+ control: i.controls.select({
744
+ options: async ({ state }) => {
745
+ // Check cache first
746
+ if (state.endpointCache) {
747
+ return state.endpointCache;
748
+ }
749
+
750
+ // Fetch from API
751
+ const endpoints = await state.apiClient.getEndpoints();
752
+ const options = endpoints.map(ep => ({
753
+ value: ep.id,
754
+ label: ep.name,
755
+ description: `${ep.method} ${ep.path}`,
756
+ }));
757
+
758
+ // Cache results
759
+ state.endpointCache = options;
760
+
761
+ return options;
762
+ },
763
+ }),
764
+ }),
765
+ ```
766
+
767
+ ## Type Safety and Inference
768
+
769
+ The framework provides comprehensive TypeScript support:
770
+
771
+ ```typescript
772
+ // Type inference from integration definition
773
+ const myIntegration = i.integration({
774
+ nodes: {
775
+ processor: i.nodes.callable({
776
+ inputs: {
777
+ data: i.pins.data(),
778
+ },
779
+ outputs: {
780
+ result: i.pins.data(),
781
+ },
782
+ run({ inputs, next }) {
783
+ // inputs.data is properly typed
784
+ // next is properly typed
785
+ next({ result: inputs.data });
786
+ },
787
+ }),
788
+ },
789
+ });
790
+
791
+ // Extract types from integration
792
+ type IntegrationOutput = typeof myIntegration.$infer;
793
+ // IntegrationOutput.nodes.processor.inputs.data is typed
794
+ // IntegrationOutput.nodes.processor.outputs.result is typed
795
+ ```
796
+
797
+ ## Development Best Practices
798
+
799
+ 1. **Use TypeScript**: The framework is built for TypeScript - use it
800
+ 2. **Descriptive Names**: Use clear, descriptive names for nodes, pins, and variables
801
+ 3. **Categories**: Organize nodes with categories for better UX
802
+ 4. **Documentation**: Add descriptions to nodes and pins for AI assistance
803
+ 5. **Examples**: Provide examples for complex data pins
804
+ 6. **State Management**: Use integration state for shared resources
805
+ 7. **Error Handling**: Always throw errors, never handle them with exec pins
806
+ 8. **Cleanup**: Always return cleanup functions from trigger subscriptions
807
+ 9. **Optional Pins**: Use optional pins to reduce UI clutter
808
+ 10. **Validation**: Use schema validation for robust data handling
809
+
810
+ ## Testing Guidelines
811
+
812
+ ```typescript
813
+ // Test pure nodes easily
814
+ import { describe, expect, it } from 'vitest';
815
+
816
+ import { integration } from './my-integration';
817
+
818
+ describe('Data Transform Node', () => {
819
+ it('should transform data correctly', () => {
820
+ const node = integration.nodes.dataTransform;
821
+ const outputs = {};
822
+
823
+ node.run({
824
+ inputs: { data: { name: 'John' }, mapping: { title: 'data.name' } },
825
+ outputs,
826
+ state: {},
827
+ ctx: {},
828
+ variables: {},
829
+ webhook: { url: 'http://test' },
830
+ });
831
+
832
+ expect(outputs.result).toEqual({ title: 'John' });
833
+ });
834
+ });
835
+ ```
836
+
837
+ This framework enables you to build powerful, type-safe integrations with clear separation of concerns and excellent developer experience.