@xentom/integration-framework 0.0.18 → 0.0.19
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/AGENTS.md +438 -0
- package/package.json +4 -4
- package/CLAUDE.md +0 -837
package/AGENTS.md
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
# @xentom/integration-framework — Agent Guide
|
|
2
|
+
|
|
3
|
+
Concise, directive reference for AI agents building integrations with this framework.
|
|
4
|
+
This is the only file you need to read. For type-level details, check the `.d.ts` files in `dist/`.
|
|
5
|
+
|
|
6
|
+
## Import Convention
|
|
7
|
+
|
|
8
|
+
```typescript
|
|
9
|
+
import * as i from '@xentom/integration-framework'
|
|
10
|
+
import * as v from 'valibot'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Always use namespace imports for the framework (`i`) and validator (`v`).
|
|
14
|
+
Any [Standard Schema](https://github.com/standard-schema/standard-schema)-compatible validator works (e.g. `valibot`, `zod`). Most integrations use `valibot`.
|
|
15
|
+
Never access the validator through the framework (no `i.v.string()`).
|
|
16
|
+
|
|
17
|
+
## Integration Structure
|
|
18
|
+
|
|
19
|
+
Every integration default-exports a call to `i.integration()`:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
declare module '@xentom/integration-framework' {
|
|
23
|
+
interface IntegrationState {
|
|
24
|
+
client: SomeClient
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default i.integration({
|
|
29
|
+
nodes, // required — record of trigger/callable/pure nodes
|
|
30
|
+
auth, // optional — i.auth.token(), i.auth.oauth2(), or i.auth.basic()
|
|
31
|
+
env, // optional — record of i.env() definitions
|
|
32
|
+
start(opts) { // optional — initialize shared resources
|
|
33
|
+
opts.state.client = new SomeClient(opts.auth.token)
|
|
34
|
+
},
|
|
35
|
+
stop(opts) { // optional — tear down resources
|
|
36
|
+
opts.state.client.close()
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- Augment `IntegrationState` via `declare module` to type the shared `state` object.
|
|
42
|
+
- `start` receives `{ auth, env, state, webhook, kv }`.
|
|
43
|
+
- `stop` receives the same, but `state` values may be undefined (partial).
|
|
44
|
+
- `state` is in-memory only — not persisted across restarts.
|
|
45
|
+
- When using valibot, set global config for early abort at the top of your entry point:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
v.setGlobalConfig({ abortEarly: true, abortPipeEarly: true })
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Authentication
|
|
52
|
+
|
|
53
|
+
Three auth strategies. Pick one per integration.
|
|
54
|
+
|
|
55
|
+
| Strategy | Builder | Credential access |
|
|
56
|
+
|----------|---------|-------------------|
|
|
57
|
+
| API token | `i.auth.token()` | `opts.auth.token` (string) |
|
|
58
|
+
| OAuth 2.0 | `i.auth.oauth2({ authUrl, tokenUrl, scopes })` | `opts.auth.accessToken` (string) |
|
|
59
|
+
| Basic | `i.auth.basic()` | `opts.auth.username`, `opts.auth.password` |
|
|
60
|
+
|
|
61
|
+
Token auth supports a `schema` for validation and a `control` for the UI:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
auth: i.auth.token({
|
|
65
|
+
control: i.controls.text({
|
|
66
|
+
label: 'API Key',
|
|
67
|
+
placeholder: 'sk_...',
|
|
68
|
+
}),
|
|
69
|
+
schema: v.pipeAsync(
|
|
70
|
+
v.string(),
|
|
71
|
+
v.startsWith('sk_'),
|
|
72
|
+
v.checkAsync(async (token) => { /* test call */ return true }, 'Invalid key.'),
|
|
73
|
+
),
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
OAuth2 also supports `grantType`, `pkce`, `pkceMethod`, and `onAccessTokenUpdated`.
|
|
78
|
+
When using OAuth2, implement `onAccessTokenUpdated` to refresh your client when tokens rotate:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
auth: i.auth.oauth2({
|
|
82
|
+
authUrl: 'https://example.com/oauth/authorize',
|
|
83
|
+
tokenUrl: 'https://example.com/oauth/token',
|
|
84
|
+
scopes: ['read', 'write'],
|
|
85
|
+
onAccessTokenUpdated(opts) {
|
|
86
|
+
opts.state.client.setToken(opts.auth.accessToken)
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Environment Variables
|
|
92
|
+
|
|
93
|
+
Use `env` for configuration values that are not authentication credentials.
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
env: {
|
|
97
|
+
REGION: i.env({
|
|
98
|
+
control: i.controls.select({
|
|
99
|
+
options: [
|
|
100
|
+
{ value: 'us-east-1', label: 'US East' },
|
|
101
|
+
{ value: 'eu-west-1', label: 'EU West' },
|
|
102
|
+
],
|
|
103
|
+
}),
|
|
104
|
+
}),
|
|
105
|
+
DEBUG: i.env({
|
|
106
|
+
control: i.controls.switch({ label: 'Debug Mode', defaultValue: false }),
|
|
107
|
+
optional: true,
|
|
108
|
+
}),
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Rules:
|
|
113
|
+
- Env vars are available in `start`/`stop` via `opts.env`. They are **not** available inside nodes.
|
|
114
|
+
- To use env values in nodes, read them in `start` and store on `state`.
|
|
115
|
+
- Controls are limited to `text`, `switch`, and `select` (static options only — no async callback).
|
|
116
|
+
- Set `sensitive: true` on text controls for secrets.
|
|
117
|
+
- Add `optional: true` on the env definition itself (not the control) if the value is not required.
|
|
118
|
+
|
|
119
|
+
## Node Types
|
|
120
|
+
|
|
121
|
+
### Trigger — workflow entry point
|
|
122
|
+
|
|
123
|
+
Listens for external events. Implements `subscribe`, which should return a cleanup function.
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
export const onEvent = nodes.trigger({
|
|
127
|
+
outputs: {
|
|
128
|
+
payload: i.pins.data<EventPayload>(),
|
|
129
|
+
},
|
|
130
|
+
subscribe(opts) {
|
|
131
|
+
const unsubscribe = opts.state.client.on('event', (data) => {
|
|
132
|
+
void opts.next({ payload: data })
|
|
133
|
+
})
|
|
134
|
+
return () => unsubscribe()
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
- `subscribe` receives `{ next, state, webhook, inputs, variables, kv, node, workflow }`.
|
|
140
|
+
- Call `next(outputs)` to fire the trigger. If exec pins are defined, call `next('pinName', outputs)`.
|
|
141
|
+
- Use `void opts.next(...)` for fire-and-forget, or `await opts.next(...)` when you need to wait for downstream execution.
|
|
142
|
+
- `next` accepts an optional trailing arg for context and dedup: `next(outputs, { ctx, deduplication: { id, ttl? } })` or `next('pinName', outputs, { ctx, deduplication: { id, ttl? } })`.
|
|
143
|
+
- Webhook triggers: call `opts.webhook.subscribe(handler)` — handler receives a `Request`, returns a `Response`.
|
|
144
|
+
- The `workflow.on('start', callback)` hook lets you react to workflow lifecycle events.
|
|
145
|
+
|
|
146
|
+
### Callable — side-effect action
|
|
147
|
+
|
|
148
|
+
Invoked by other nodes during a workflow run. Implements `run` and calls `next()` to continue the workflow.
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
export const createItem = nodes.callable({
|
|
152
|
+
inputs: {
|
|
153
|
+
name: pins.item.name,
|
|
154
|
+
},
|
|
155
|
+
outputs: {
|
|
156
|
+
id: pins.item.id.with({ control: false }),
|
|
157
|
+
},
|
|
158
|
+
async run(opts) {
|
|
159
|
+
const result = await opts.state.client.items.create({ name: opts.inputs.name })
|
|
160
|
+
return opts.next({ id: result.id })
|
|
161
|
+
},
|
|
162
|
+
})
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
- `run` receives `{ next, state, inputs, ctx, variables, webhook, kv, node }`.
|
|
166
|
+
- Always `return opts.next(...)` — this is the standard pattern across all integrations.
|
|
167
|
+
- If the node defines exec pins in outputs, call `return opts.next('pinName', outputs)` for the specific branch.
|
|
168
|
+
- Omitting `next()` is valid for terminal/void actions (e.g. delete operations) that have no downstream outputs. The workflow will not continue to connected nodes.
|
|
169
|
+
|
|
170
|
+
### Pure — deterministic transform
|
|
171
|
+
|
|
172
|
+
Side-effect-free computation. Assign directly to `outputs` — no `next()` call.
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
export const formatName = nodes.pure({
|
|
176
|
+
inputs: {
|
|
177
|
+
first: i.pins.data({ schema: v.string() }),
|
|
178
|
+
last: i.pins.data({ schema: v.string() }),
|
|
179
|
+
},
|
|
180
|
+
outputs: {
|
|
181
|
+
full: i.pins.data<string>(),
|
|
182
|
+
},
|
|
183
|
+
run(opts) {
|
|
184
|
+
opts.outputs.full = `${opts.inputs.first} ${opts.inputs.last}`
|
|
185
|
+
},
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
- `run` receives `{ inputs, outputs, state, ctx, variables, webhook, kv, node }`.
|
|
190
|
+
- `run` is optional. Data-only pure nodes (e.g. a string constant with just a control) can omit it entirely.
|
|
191
|
+
- Pure nodes cannot have exec pins. Outputs must be data pins only.
|
|
192
|
+
- Evaluated lazily when downstream nodes need their outputs.
|
|
193
|
+
|
|
194
|
+
### When to pick which
|
|
195
|
+
|
|
196
|
+
| Need | Node type |
|
|
197
|
+
|------|-----------|
|
|
198
|
+
| React to external events (webhooks, timers, client events) | `trigger` |
|
|
199
|
+
| Call an API, mutate data, produce side effects | `callable` |
|
|
200
|
+
| Transform or compute a value from inputs, no side effects | `pure` |
|
|
201
|
+
|
|
202
|
+
## Node Groups
|
|
203
|
+
|
|
204
|
+
Organize nodes into UI categories:
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
const nodes = i.nodes.group('Emails')
|
|
208
|
+
|
|
209
|
+
export const sendEmail = nodes.callable({ ... })
|
|
210
|
+
export const listEmails = nodes.callable({ ... })
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Use slash-separated names for nested categories:
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
const nodes = i.nodes.group('Repositories/Issues')
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`group()` returns a scoped builder with `trigger`, `callable`, and `pure` — but no nested `group`.
|
|
220
|
+
|
|
221
|
+
## Pins
|
|
222
|
+
|
|
223
|
+
### Data Pins
|
|
224
|
+
|
|
225
|
+
Carry values between nodes.
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
// Minimal
|
|
229
|
+
i.pins.data()
|
|
230
|
+
|
|
231
|
+
// Typed (compile-time only)
|
|
232
|
+
i.pins.data<User>()
|
|
233
|
+
|
|
234
|
+
// With schema (runtime validation)
|
|
235
|
+
i.pins.data({ schema: v.pipe(v.string(), v.email()) })
|
|
236
|
+
|
|
237
|
+
// With state-dependent schema (function form — receives IntegrationOptions)
|
|
238
|
+
i.pins.data({ schema: ({ state }) => v.pipe(v.string(), v.transform((id) => state.client.getById(id))) })
|
|
239
|
+
|
|
240
|
+
// With control
|
|
241
|
+
i.pins.data({
|
|
242
|
+
description: 'Recipient email.',
|
|
243
|
+
schema: v.pipe(v.string(), v.email()),
|
|
244
|
+
control: i.controls.text({ placeholder: 'user@example.com' }),
|
|
245
|
+
})
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
#### Customizing with `.with()`
|
|
249
|
+
|
|
250
|
+
Always use `.with()` to extend a pin definition. Never destructure or access internal properties.
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
pins.email.address.with({ optional: true, description: 'CC recipient.' })
|
|
254
|
+
pins.item.id.with({ control: false }) // strip the control (e.g. for output-only pins)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
#### Pin definition rules
|
|
258
|
+
|
|
259
|
+
- **Reusable pins** (in `src/pins/`): never set `optional: true` directly. Apply it via `.with()` at the usage site so the pin stays reusable. Same for `control: false` — use `.with({ control: false })` to strip controls when using a pin as an output.
|
|
260
|
+
- **Inline pins** (created directly in node inputs/outputs): setting `optional: true` or `control: false` directly is fine.
|
|
261
|
+
- Naming: `item` for a single object, `items` for an array, descriptive names for properties (`id`, `name`, `email`).
|
|
262
|
+
- Provide `examples` on complex pins to help users and AI.
|
|
263
|
+
|
|
264
|
+
### Exec Pins
|
|
265
|
+
|
|
266
|
+
Control execution branching in trigger and callable nodes.
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
outputs: {
|
|
270
|
+
forEach: i.pins.exec({
|
|
271
|
+
outputs: {
|
|
272
|
+
item: i.pins.data(),
|
|
273
|
+
index: i.pins.data(),
|
|
274
|
+
},
|
|
275
|
+
}),
|
|
276
|
+
completed: i.pins.exec(),
|
|
277
|
+
},
|
|
278
|
+
async run(opts) {
|
|
279
|
+
for (const [index, item] of opts.inputs.items.entries()) {
|
|
280
|
+
await opts.next('forEach', { item, index })
|
|
281
|
+
}
|
|
282
|
+
return opts.next('completed')
|
|
283
|
+
},
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Only use exec pins for:**
|
|
287
|
+
1. Branching logic (conditional paths)
|
|
288
|
+
2. Iteration (forEach patterns)
|
|
289
|
+
3. State machines
|
|
290
|
+
|
|
291
|
+
**Never use exec pins for error handling.** Throw instead.
|
|
292
|
+
|
|
293
|
+
## Controls
|
|
294
|
+
|
|
295
|
+
| Control | Use for | Key options |
|
|
296
|
+
|---------|---------|-------------|
|
|
297
|
+
| `i.controls.text()` | Strings | `placeholder`, `sensitive`, `rows`, `language` (`'plain'`, `'html'`, `'markdown'`, `'javascript'`) |
|
|
298
|
+
| `i.controls.expression()` | Numbers, objects, JS expressions | `placeholder`, `rows`, `defaultValue` |
|
|
299
|
+
| `i.controls.select()` | Enum/choice values | `options` (static array or async callback), `placeholder`, `multiple` |
|
|
300
|
+
| `i.controls.switch()` | Booleans | `defaultValue` |
|
|
301
|
+
|
|
302
|
+
### Dynamic select options (nodes only)
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
i.controls.select({
|
|
306
|
+
async options({ state, pagination, search }) {
|
|
307
|
+
const res = await state.client.items.list({ limit: pagination.limit, after: pagination.after })
|
|
308
|
+
return {
|
|
309
|
+
items: res.data.map((item) => ({ value: item.id, label: item.name, suffix: item.id })),
|
|
310
|
+
hasMore: res.hasMore,
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
})
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
The callback receives `{ state, auth, env, webhook, kv, node, pagination, search }`.
|
|
317
|
+
`pagination` has `limit`, `after?`, `before?`, and `page`.
|
|
318
|
+
Dynamic options are **not** available on env var controls.
|
|
319
|
+
|
|
320
|
+
## Generic Nodes
|
|
321
|
+
|
|
322
|
+
Use `i.generic()` when a node's output types depend on its input types at the type level:
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
export const passthrough = i.generic(<I extends i.GenericInputs<typeof inputs>>() => {
|
|
326
|
+
const inputs = {
|
|
327
|
+
value: i.pins.data(),
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return i.nodes.pure({
|
|
331
|
+
inputs,
|
|
332
|
+
outputs: {
|
|
333
|
+
value: i.pins.data<I['value']>(),
|
|
334
|
+
},
|
|
335
|
+
run({ inputs, outputs }) {
|
|
336
|
+
outputs.value = inputs.value
|
|
337
|
+
},
|
|
338
|
+
})
|
|
339
|
+
})
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
This is an advanced pattern. Use it only when static typing cannot express the relationship between inputs and outputs.
|
|
343
|
+
|
|
344
|
+
## Key-Value Store
|
|
345
|
+
|
|
346
|
+
A persistent store available via `opts.kv` in all nodes, triggers, and lifecycle hooks.
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
await opts.kv.set('cursor', lastCursor)
|
|
350
|
+
const cursor = await opts.kv.get('cursor') // string | null
|
|
351
|
+
await opts.kv.delete('cursor', 'otherKey')
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Values are strings. Persists across restarts. Use for pagination cursors, sync tokens, or cached state.
|
|
355
|
+
|
|
356
|
+
## Error Handling
|
|
357
|
+
|
|
358
|
+
**Always throw. Never route errors through exec pins or return values.**
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
// Correct — throw and let the framework handle it
|
|
362
|
+
async run(opts) {
|
|
363
|
+
const res = await opts.state.client.items.get(opts.inputs.id)
|
|
364
|
+
if (!res) throw new Error('Item not found')
|
|
365
|
+
return opts.next({ item: res })
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
The framework catches thrown errors and surfaces them to the user.
|
|
370
|
+
|
|
371
|
+
The only exception is webhook handlers inside triggers, which may catch errors to return an appropriate HTTP response:
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
subscribe(opts) {
|
|
375
|
+
const unsub = opts.webhook.subscribe(async (req) => {
|
|
376
|
+
try {
|
|
377
|
+
const data = await req.json()
|
|
378
|
+
opts.next({ data })
|
|
379
|
+
return new Response('OK')
|
|
380
|
+
} catch {
|
|
381
|
+
return new Response('Bad Request', { status: 400 })
|
|
382
|
+
}
|
|
383
|
+
})
|
|
384
|
+
return () => unsub()
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Trigger Cleanup
|
|
389
|
+
|
|
390
|
+
Every `subscribe` function should return a cleanup function (sync or async). The framework calls it when the workflow stops or reconfigures.
|
|
391
|
+
|
|
392
|
+
Clean up:
|
|
393
|
+
- `setInterval` / `setTimeout` handles
|
|
394
|
+
- Event listener subscriptions
|
|
395
|
+
- Webhook handlers (the return value of `webhook.subscribe()`)
|
|
396
|
+
- Any open connections or resources
|
|
397
|
+
|
|
398
|
+
Omitting cleanup is technically allowed (the return type permits `void`), but causes resource leaks in most cases. Only omit it when cleanup is handled centrally (e.g. in `stop()`).
|
|
399
|
+
|
|
400
|
+
## Webhook Routing in `start()`
|
|
401
|
+
|
|
402
|
+
For integrations that receive all events through a single webhook endpoint (e.g. GitHub, Stripe), set up central webhook routing in `start()` and distribute events to trigger nodes via shared state:
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
start(opts) {
|
|
406
|
+
opts.state.client = new Client(opts.auth.token)
|
|
407
|
+
opts.state.events = new EventEmitter()
|
|
408
|
+
|
|
409
|
+
opts.webhook.subscribe(async (request) => {
|
|
410
|
+
const signature = request.headers.get('X-Signature')
|
|
411
|
+
if (!signature) return new Response('Unauthorized', { status: 401 })
|
|
412
|
+
|
|
413
|
+
const payload = await request.text()
|
|
414
|
+
if (!verify(payload, signature)) return new Response('Forbidden', { status: 403 })
|
|
415
|
+
|
|
416
|
+
const event = JSON.parse(payload)
|
|
417
|
+
opts.state.events.emit(event.type, event)
|
|
418
|
+
return new Response('OK')
|
|
419
|
+
})
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Individual trigger nodes then subscribe to the shared event emitter in their `subscribe()` function.
|
|
424
|
+
|
|
425
|
+
## Common Mistakes
|
|
426
|
+
|
|
427
|
+
- **Forgetting `return opts.next()` in callable nodes.** The workflow will not continue to downstream nodes. Always `return opts.next(...)` unless the node is a terminal/void action.
|
|
428
|
+
- **Calling `next()` in pure nodes.** Pure nodes assign to `outputs` directly. They have no `next`.
|
|
429
|
+
- **Using exec pins for error handling.** Throw errors instead. The framework handles them.
|
|
430
|
+
- **Not returning cleanup from `subscribe`.** Causes resource leaks on stop/reconfigure.
|
|
431
|
+
- **Accessing `env` inside nodes.** Env vars are only in `start`/`stop`. Store values on `state` instead.
|
|
432
|
+
- **Setting `optional: true` on reusable pin definitions (in `src/pins/`).** Apply it via `.with()` at the usage site to keep the pin reusable. Inline pins in node definitions may set it directly.
|
|
433
|
+
- **Using `as any`, `@ts-ignore`, or `biome-ignore`.** Fix the type error properly. If stuck after 3 attempts, redesign the approach. The only acceptable escape hatch is `@ts-expect-error` in rare generic type narrowing scenarios.
|
|
434
|
+
- **Using `import type { X } from 'pkg'`.** Use inline type keyword: `import { type X } from 'pkg'`.
|
|
435
|
+
- **Generic export names.** Use `sendEmail`, not `send`. Use `deleteUser`, not `delete`.
|
|
436
|
+
- **Unnecessary `displayName`.** Only set it when auto-generation from the export name is wrong (e.g. acronyms, JS keyword conflicts).
|
|
437
|
+
- **Wrapping everything in try-catch.** Let errors propagate. Only catch when you need to transform the error or handle webhook responses.
|
|
438
|
+
- **Intermediate variables for API calls.** Prefer `opts.state.client.items.create(...)` over `const client = opts.state.client; const items = client.items; ...`.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xentom/integration-framework",
|
|
3
3
|
"description": "A type-safe, declarative framework for building composable workflow integrations using nodes, pins, and rich controls.",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.19",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://xentom.com",
|
|
7
7
|
"author": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"sideEffects": false,
|
|
43
43
|
"files": [
|
|
44
44
|
"dist",
|
|
45
|
-
"
|
|
45
|
+
"AGENTS.md"
|
|
46
46
|
],
|
|
47
47
|
"publishConfig": {
|
|
48
48
|
"access": "public"
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@xentom/style-guide": "^0.0.0",
|
|
61
|
-
"bun
|
|
61
|
+
"@types/bun": "^1.3.10",
|
|
62
62
|
"typescript": "^5.9.3"
|
|
63
63
|
}
|
|
64
|
-
}
|
|
64
|
+
}
|
package/CLAUDE.md
DELETED
|
@@ -1,837 +0,0 @@
|
|
|
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.
|