@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 +837 -836
- package/README.md +203 -0
- package/dist/auth/basic.d.ts +56 -0
- package/dist/auth/basic.js +0 -0
- package/dist/auth/index.d.ts +18 -0
- package/dist/auth/index.js +23 -0
- package/dist/auth/oauth2.d.ts +26 -0
- package/dist/auth/oauth2.js +10 -0
- package/dist/auth/token.d.ts +40 -0
- package/dist/auth/token.js +0 -0
- package/dist/controls/base.d.ts +2 -0
- package/dist/controls/expression.d.ts +5 -2
- package/dist/controls/index.d.ts +14 -1
- package/dist/controls/select.d.ts +41 -5
- package/dist/controls/switch.d.ts +3 -0
- package/dist/controls/text.d.ts +3 -0
- package/dist/env.d.ts +17 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/integration.d.ts +55 -13
- package/dist/integration.js +0 -2
- package/dist/nodes/base.d.ts +8 -13
- package/dist/nodes/callable.d.ts +11 -0
- package/dist/nodes/index.d.ts +15 -10
- package/dist/nodes/index.js +17 -0
- package/dist/nodes/pure.d.ts +9 -4
- package/dist/nodes/trigger.d.ts +10 -1
- package/dist/nodes/utils.d.ts +1 -0
- package/dist/nodes/utils.js +1 -0
- package/dist/pins/data.d.ts +34 -15
- package/dist/pins/exec.d.ts +11 -2
- package/dist/pins/index.d.ts +6 -0
- package/dist/pins/utils.d.ts +4 -2
- package/dist/utils.d.ts +1 -0
- package/package.json +6 -4
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,
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
const
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
});
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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.
|