@q1k-oss/btree-workflows 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +31 -0
- package/CLAUDE.md +181 -0
- package/LICENSE +21 -0
- package/README.md +920 -0
- package/behaviour-tree-workflows-landing/index.html +16 -0
- package/behaviour-tree-workflows-landing/package-lock.json +2074 -0
- package/behaviour-tree-workflows-landing/package.json +31 -0
- package/behaviour-tree-workflows-landing/public/favicon.svg +17 -0
- package/behaviour-tree-workflows-landing/src/App.css +103 -0
- package/behaviour-tree-workflows-landing/src/App.tsx +176 -0
- package/behaviour-tree-workflows-landing/src/components/BlackboardInspector.css +89 -0
- package/behaviour-tree-workflows-landing/src/components/BlackboardInspector.tsx +64 -0
- package/behaviour-tree-workflows-landing/src/components/ExampleSelector.css +64 -0
- package/behaviour-tree-workflows-landing/src/components/ExampleSelector.tsx +34 -0
- package/behaviour-tree-workflows-landing/src/components/ExecutionLog.css +107 -0
- package/behaviour-tree-workflows-landing/src/components/ExecutionLog.tsx +85 -0
- package/behaviour-tree-workflows-landing/src/components/Header.css +50 -0
- package/behaviour-tree-workflows-landing/src/components/Header.tsx +26 -0
- package/behaviour-tree-workflows-landing/src/components/StatusBadge.css +45 -0
- package/behaviour-tree-workflows-landing/src/components/StatusBadge.tsx +15 -0
- package/behaviour-tree-workflows-landing/src/components/Toolbar.css +74 -0
- package/behaviour-tree-workflows-landing/src/components/Toolbar.tsx +53 -0
- package/behaviour-tree-workflows-landing/src/components/TreeVisualizer.css +67 -0
- package/behaviour-tree-workflows-landing/src/components/TreeVisualizer.tsx +192 -0
- package/behaviour-tree-workflows-landing/src/components/YamlEditor.css +18 -0
- package/behaviour-tree-workflows-landing/src/components/YamlEditor.tsx +96 -0
- package/behaviour-tree-workflows-landing/src/lib/count-nodes.ts +11 -0
- package/behaviour-tree-workflows-landing/src/lib/execution-engine.ts +96 -0
- package/behaviour-tree-workflows-landing/src/lib/tree-layout.ts +136 -0
- package/behaviour-tree-workflows-landing/src/lib/yaml-examples.ts +549 -0
- package/behaviour-tree-workflows-landing/src/main.tsx +9 -0
- package/behaviour-tree-workflows-landing/src/stubs/activepieces.ts +18 -0
- package/behaviour-tree-workflows-landing/src/stubs/fs.ts +24 -0
- package/behaviour-tree-workflows-landing/src/stubs/path.ts +16 -0
- package/behaviour-tree-workflows-landing/src/stubs/temporal-activity.ts +6 -0
- package/behaviour-tree-workflows-landing/src/stubs/temporal-workflow.ts +22 -0
- package/behaviour-tree-workflows-landing/tsconfig.json +25 -0
- package/behaviour-tree-workflows-landing/vite.config.ts +40 -0
- package/demo-google-sheets.ts +181 -0
- package/demo-runtime-variables.ts +174 -0
- package/demo-template.ts +208 -0
- package/docs/ARCHITECTURE_SUMMARY.md +613 -0
- package/docs/NODE_REFERENCE.md +504 -0
- package/docs/README.md +53 -0
- package/docs/custom-nodes-architecture.md +826 -0
- package/docs/observability.md +175 -0
- package/docs/yaml-specification.md +990 -0
- package/examples/temporal/README.md +117 -0
- package/examples/temporal/activities.ts +373 -0
- package/examples/temporal/client.ts +115 -0
- package/examples/temporal/python-worker/activities.py +339 -0
- package/examples/temporal/python-worker/requirements.txt +12 -0
- package/examples/temporal/python-worker/worker.py +106 -0
- package/examples/temporal/worker.ts +66 -0
- package/examples/temporal/workflows.ts +6 -0
- package/examples/temporal/yaml-workflow-loader.ts +105 -0
- package/examples/yaml-test.ts +97 -0
- package/examples/yaml-workflows/01-simple-sequence.yaml +25 -0
- package/examples/yaml-workflows/02-parallel-timeout.yaml +45 -0
- package/examples/yaml-workflows/03-ecommerce-checkout.yaml +94 -0
- package/examples/yaml-workflows/04-ai-agent-workflow.yaml +346 -0
- package/examples/yaml-workflows/05-order-processing.yaml +146 -0
- package/examples/yaml-workflows/06-activity-test.yaml +71 -0
- package/examples/yaml-workflows/07-activity-simple-test.yaml +43 -0
- package/examples/yaml-workflows/08-file-processing.yaml +141 -0
- package/examples/yaml-workflows/09-http-request.yaml +137 -0
- package/examples/yaml-workflows/README.md +211 -0
- package/package.json +38 -0
- package/src/actions/code-execution.schema.ts +27 -0
- package/src/actions/code-execution.ts +218 -0
- package/src/actions/generate-file.test.ts +516 -0
- package/src/actions/generate-file.ts +166 -0
- package/src/actions/http-request.test.ts +784 -0
- package/src/actions/http-request.ts +228 -0
- package/src/actions/index.ts +20 -0
- package/src/actions/parse-file.test.ts +448 -0
- package/src/actions/parse-file.ts +139 -0
- package/src/actions/python-script.test.ts +439 -0
- package/src/actions/python-script.ts +154 -0
- package/src/base-node.test.ts +511 -0
- package/src/base-node.ts +605 -0
- package/src/behavior-tree.test.ts +431 -0
- package/src/behavior-tree.ts +283 -0
- package/src/blackboard.test.ts +222 -0
- package/src/blackboard.ts +192 -0
- package/src/composites/conditional.schema.ts +19 -0
- package/src/composites/conditional.test.ts +309 -0
- package/src/composites/conditional.ts +129 -0
- package/src/composites/for-each.schema.ts +23 -0
- package/src/composites/for-each.test.ts +254 -0
- package/src/composites/for-each.ts +132 -0
- package/src/composites/index.ts +15 -0
- package/src/composites/memory-sequence.schema.ts +19 -0
- package/src/composites/memory-sequence.test.ts +223 -0
- package/src/composites/memory-sequence.ts +98 -0
- package/src/composites/parallel.schema.ts +28 -0
- package/src/composites/parallel.test.ts +502 -0
- package/src/composites/parallel.ts +157 -0
- package/src/composites/reactive-sequence.schema.ts +19 -0
- package/src/composites/reactive-sequence.test.ts +170 -0
- package/src/composites/reactive-sequence.ts +85 -0
- package/src/composites/recovery.schema.ts +19 -0
- package/src/composites/recovery.test.ts +366 -0
- package/src/composites/recovery.ts +90 -0
- package/src/composites/selector.schema.ts +19 -0
- package/src/composites/selector.test.ts +387 -0
- package/src/composites/selector.ts +85 -0
- package/src/composites/sequence.schema.ts +19 -0
- package/src/composites/sequence.test.ts +337 -0
- package/src/composites/sequence.ts +72 -0
- package/src/composites/sub-tree.schema.ts +21 -0
- package/src/composites/sub-tree.test.ts +893 -0
- package/src/composites/sub-tree.ts +177 -0
- package/src/composites/while.schema.ts +24 -0
- package/src/composites/while.test.ts +381 -0
- package/src/composites/while.ts +149 -0
- package/src/data-store/index.ts +10 -0
- package/src/data-store/memory-store.ts +161 -0
- package/src/data-store/types.ts +94 -0
- package/src/debug/breakpoint.test.ts +47 -0
- package/src/debug/breakpoint.ts +30 -0
- package/src/debug/index.ts +17 -0
- package/src/debug/resume-point.test.ts +49 -0
- package/src/debug/resume-point.ts +29 -0
- package/src/decorators/delay.schema.ts +21 -0
- package/src/decorators/delay.test.ts +261 -0
- package/src/decorators/delay.ts +140 -0
- package/src/decorators/force-result.schema.ts +32 -0
- package/src/decorators/force-result.test.ts +133 -0
- package/src/decorators/force-result.ts +63 -0
- package/src/decorators/index.ts +13 -0
- package/src/decorators/invert.schema.ts +19 -0
- package/src/decorators/invert.test.ts +135 -0
- package/src/decorators/invert.ts +42 -0
- package/src/decorators/keep-running.schema.ts +20 -0
- package/src/decorators/keep-running.test.ts +105 -0
- package/src/decorators/keep-running.ts +49 -0
- package/src/decorators/precondition.schema.ts +19 -0
- package/src/decorators/precondition.test.ts +351 -0
- package/src/decorators/precondition.ts +139 -0
- package/src/decorators/repeat.schema.ts +21 -0
- package/src/decorators/repeat.test.ts +187 -0
- package/src/decorators/repeat.ts +94 -0
- package/src/decorators/run-once.schema.ts +19 -0
- package/src/decorators/run-once.test.ts +140 -0
- package/src/decorators/run-once.ts +61 -0
- package/src/decorators/soft-assert.schema.ts +19 -0
- package/src/decorators/soft-assert.test.ts +107 -0
- package/src/decorators/soft-assert.ts +68 -0
- package/src/decorators/timeout.schema.ts +21 -0
- package/src/decorators/timeout.test.ts +274 -0
- package/src/decorators/timeout.ts +159 -0
- package/src/errors.test.ts +63 -0
- package/src/errors.ts +34 -0
- package/src/events.test.ts +347 -0
- package/src/events.ts +183 -0
- package/src/index.ts +80 -0
- package/src/integrations/index.ts +30 -0
- package/src/integrations/integration-action.test.ts +571 -0
- package/src/integrations/integration-action.ts +233 -0
- package/src/integrations/piece-executor.ts +320 -0
- package/src/observability/execution-tracker.ts +320 -0
- package/src/observability/index.ts +23 -0
- package/src/observability/sinks.ts +138 -0
- package/src/observability/types.ts +130 -0
- package/src/registry-utils.ts +147 -0
- package/src/registry.test.ts +466 -0
- package/src/registry.ts +334 -0
- package/src/schemas/base.schema.ts +104 -0
- package/src/schemas/index.ts +223 -0
- package/src/schemas/integration.test.ts +238 -0
- package/src/schemas/tree-definition.schema.ts +170 -0
- package/src/schemas/validation.test.ts +146 -0
- package/src/schemas/validation.ts +122 -0
- package/src/scripting/index.ts +22 -0
- package/src/templates/template-loader.test.ts +281 -0
- package/src/templates/template-loader.ts +152 -0
- package/src/temporal-integration.test.ts +213 -0
- package/src/test-nodes.ts +259 -0
- package/src/types.ts +503 -0
- package/src/utilities/index.ts +17 -0
- package/src/utilities/log-message.test.ts +275 -0
- package/src/utilities/log-message.ts +134 -0
- package/src/utilities/regex-extract.test.ts +138 -0
- package/src/utilities/regex-extract.ts +108 -0
- package/src/utilities/variable-resolver.test.ts +416 -0
- package/src/utilities/variable-resolver.ts +318 -0
- package/src/utils/error-handler.test.ts +117 -0
- package/src/utils/error-handler.ts +48 -0
- package/src/utils/signal-check.test.ts +234 -0
- package/src/utils/signal-check.ts +140 -0
- package/src/yaml/errors.ts +143 -0
- package/src/yaml/index.ts +30 -0
- package/src/yaml/loader.ts +39 -0
- package/src/yaml/parser.ts +286 -0
- package/src/yaml/validation/semantic-validator.ts +196 -0
- package/templates/google-sheets/insert-row.yaml +76 -0
- package/templates/notification-sender.yaml +33 -0
- package/templates/order-validation.yaml +44 -0
- package/tsconfig.json +24 -0
- package/vitest.config.ts +25 -0
- package/workflows/order-processor.yaml +59 -0
- package/workflows/process-order-workflow.yaml +142 -0
|
@@ -0,0 +1,826 @@
|
|
|
1
|
+
# Custom Nodes Plugin System Architecture
|
|
2
|
+
|
|
3
|
+
**Goal**: Allow tenants to create custom workflow nodes without writing code initially, then progressively enable full TypeScript node development.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Phased Rollout Strategy
|
|
8
|
+
|
|
9
|
+
### Phase 1 (MVP - Week 1-4): Built-in Nodes Only
|
|
10
|
+
- ✅ 32 standard nodes from `registerStandardNodes()`
|
|
11
|
+
- ✅ Python/JavaScript interpreter nodes (coming in Phase 2A)
|
|
12
|
+
- ❌ No custom uploads yet
|
|
13
|
+
|
|
14
|
+
### Phase 2A (Week 5-6): Interpreter Nodes
|
|
15
|
+
- ✅ Python interpreter node (user provides Python code inline)
|
|
16
|
+
- ✅ JavaScript interpreter node (user provides JS code inline)
|
|
17
|
+
- ✅ Sandboxed execution with timeouts and memory limits
|
|
18
|
+
|
|
19
|
+
### Phase 2B (Week 7-8): Custom Node Uploads
|
|
20
|
+
- ✅ Upload TypeScript files
|
|
21
|
+
- ✅ Server compiles and bundles
|
|
22
|
+
- ✅ Per-tenant node registry
|
|
23
|
+
- ✅ Versioning and rollback
|
|
24
|
+
|
|
25
|
+
### Phase 3 (Week 9-12): Full SDK
|
|
26
|
+
- ✅ Type-safe custom node SDK
|
|
27
|
+
- ✅ Local development with CLI
|
|
28
|
+
- ✅ Automated testing
|
|
29
|
+
- ✅ Marketplace for sharing nodes
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Phase 2A: Interpreter Nodes (Week 5-6)
|
|
34
|
+
|
|
35
|
+
### Architecture
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
YAML Workflow
|
|
39
|
+
↓
|
|
40
|
+
Contains PythonInterpreter node with inline script
|
|
41
|
+
↓
|
|
42
|
+
Worker executes node
|
|
43
|
+
↓
|
|
44
|
+
Node calls executePython activity
|
|
45
|
+
↓
|
|
46
|
+
Activity spawns Python subprocess
|
|
47
|
+
↓
|
|
48
|
+
Script executes with blackboard variables as context
|
|
49
|
+
↓
|
|
50
|
+
Result written back to blackboard
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### YAML Definition
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
type: Sequence
|
|
57
|
+
id: data-processing
|
|
58
|
+
children:
|
|
59
|
+
# Fetch data via HTTP
|
|
60
|
+
- type: HttpRequest
|
|
61
|
+
id: fetch-data
|
|
62
|
+
props:
|
|
63
|
+
url: "https://api.example.com/data"
|
|
64
|
+
method: "GET"
|
|
65
|
+
|
|
66
|
+
# Process data with Python
|
|
67
|
+
- type: PythonInterpreter
|
|
68
|
+
id: process-data
|
|
69
|
+
props:
|
|
70
|
+
outputKey: "processedData"
|
|
71
|
+
timeout: "30s"
|
|
72
|
+
script: |
|
|
73
|
+
import json
|
|
74
|
+
import numpy as np
|
|
75
|
+
|
|
76
|
+
# httpResponse comes from blackboard
|
|
77
|
+
data = httpResponse['data']
|
|
78
|
+
|
|
79
|
+
# Process data
|
|
80
|
+
values = [item['value'] for item in data]
|
|
81
|
+
mean = np.mean(values)
|
|
82
|
+
std = np.std(values)
|
|
83
|
+
|
|
84
|
+
# Result stored in blackboard under 'processedData'
|
|
85
|
+
result = {
|
|
86
|
+
'mean': mean,
|
|
87
|
+
'std': std,
|
|
88
|
+
'count': len(values)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Send processed data
|
|
92
|
+
- type: HttpRequest
|
|
93
|
+
id: send-results
|
|
94
|
+
props:
|
|
95
|
+
url: "https://api.example.com/results"
|
|
96
|
+
method: "POST"
|
|
97
|
+
body:
|
|
98
|
+
stats: "{{processedData}}"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Implementation
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// worker/src/actions/python-interpreter.ts
|
|
105
|
+
import { ActionNode } from '@wayfarer-ai/btree';
|
|
106
|
+
import { proxyActivities } from '@temporalio/workflow';
|
|
107
|
+
import type { NodeStatus, TemporalContext } from '@wayfarer-ai/btree';
|
|
108
|
+
|
|
109
|
+
interface InterpreterActivities {
|
|
110
|
+
executePython(
|
|
111
|
+
script: string,
|
|
112
|
+
context: Record<string, any>,
|
|
113
|
+
options: { timeout: string }
|
|
114
|
+
): Promise<any>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const activities = proxyActivities<InterpreterActivities>({
|
|
118
|
+
startToCloseTimeout: '5m',
|
|
119
|
+
heartbeatTimeout: '30s',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
export class PythonInterpreter extends ActionNode {
|
|
123
|
+
private script: string;
|
|
124
|
+
private outputKey: string;
|
|
125
|
+
private timeout: string;
|
|
126
|
+
|
|
127
|
+
constructor(config: any) {
|
|
128
|
+
super(config);
|
|
129
|
+
this.script = config.script;
|
|
130
|
+
this.outputKey = config.outputKey || 'pythonResult';
|
|
131
|
+
this.timeout = config.timeout || '30s';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
protected async executeTick(context: TemporalContext): Promise<NodeStatus> {
|
|
135
|
+
try {
|
|
136
|
+
// Gather blackboard data as script context
|
|
137
|
+
const scriptContext = context.blackboard.toJSON();
|
|
138
|
+
|
|
139
|
+
// Execute Python in activity
|
|
140
|
+
const result = await activities.executePython(
|
|
141
|
+
this.script,
|
|
142
|
+
scriptContext,
|
|
143
|
+
{ timeout: this.timeout }
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// Store result
|
|
147
|
+
context.blackboard.set(this.outputKey, result);
|
|
148
|
+
|
|
149
|
+
return NodeStatus.SUCCESS;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
this._lastError = `Python execution failed: ${error.message}`;
|
|
152
|
+
return NodeStatus.FAILURE;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
// worker/src/activities/python-interpreter.ts
|
|
160
|
+
import { spawn } from 'child_process';
|
|
161
|
+
import { writeFileSync, unlinkSync, mkdirSync, existsSync } from 'fs';
|
|
162
|
+
import { join } from 'path';
|
|
163
|
+
import { v4 as uuid } from 'uuid';
|
|
164
|
+
|
|
165
|
+
const PYTHON_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes max
|
|
166
|
+
|
|
167
|
+
export async function executePython(
|
|
168
|
+
script: string,
|
|
169
|
+
context: Record<string, any>,
|
|
170
|
+
options: { timeout: string }
|
|
171
|
+
): Promise<any> {
|
|
172
|
+
const scriptId = uuid();
|
|
173
|
+
const tempDir = join('/tmp', 'python-scripts');
|
|
174
|
+
|
|
175
|
+
// Create temp directory
|
|
176
|
+
if (!existsSync(tempDir)) {
|
|
177
|
+
mkdirSync(tempDir, { recursive: true });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const scriptPath = join(tempDir, `${scriptId}.py`);
|
|
181
|
+
|
|
182
|
+
// Build full Python script with context injection
|
|
183
|
+
const fullScript = `
|
|
184
|
+
import json
|
|
185
|
+
import sys
|
|
186
|
+
|
|
187
|
+
# Inject context variables from blackboard
|
|
188
|
+
${Object.entries(context)
|
|
189
|
+
.map(([key, val]) => `${key} = ${JSON.stringify(val)}`)
|
|
190
|
+
.join('\n')}
|
|
191
|
+
|
|
192
|
+
# User script
|
|
193
|
+
${script}
|
|
194
|
+
|
|
195
|
+
# Capture result variable
|
|
196
|
+
if 'result' in locals():
|
|
197
|
+
print(json.dumps(result))
|
|
198
|
+
else:
|
|
199
|
+
print(json.dumps({}))
|
|
200
|
+
`;
|
|
201
|
+
|
|
202
|
+
writeFileSync(scriptPath, fullScript);
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
// Parse timeout
|
|
206
|
+
const timeoutMs = parseTimeout(options.timeout);
|
|
207
|
+
|
|
208
|
+
// Execute Python subprocess
|
|
209
|
+
const output = await executeWithTimeout(scriptPath, timeoutMs);
|
|
210
|
+
|
|
211
|
+
// Parse JSON output
|
|
212
|
+
try {
|
|
213
|
+
return JSON.parse(output.trim());
|
|
214
|
+
} catch (e) {
|
|
215
|
+
throw new Error(`Python script did not return valid JSON: ${output}`);
|
|
216
|
+
}
|
|
217
|
+
} finally {
|
|
218
|
+
// Cleanup
|
|
219
|
+
try {
|
|
220
|
+
unlinkSync(scriptPath);
|
|
221
|
+
} catch (e) {
|
|
222
|
+
// Ignore cleanup errors
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function executeWithTimeout(
|
|
228
|
+
scriptPath: string,
|
|
229
|
+
timeoutMs: number
|
|
230
|
+
): Promise<string> {
|
|
231
|
+
return new Promise((resolve, reject) => {
|
|
232
|
+
const python = spawn('python3', [scriptPath]);
|
|
233
|
+
let stdout = '';
|
|
234
|
+
let stderr = '';
|
|
235
|
+
let timedOut = false;
|
|
236
|
+
|
|
237
|
+
// Timeout timer
|
|
238
|
+
const timer = setTimeout(() => {
|
|
239
|
+
timedOut = true;
|
|
240
|
+
python.kill('SIGTERM');
|
|
241
|
+
reject(new Error(`Python script timed out after ${timeoutMs}ms`));
|
|
242
|
+
}, timeoutMs);
|
|
243
|
+
|
|
244
|
+
python.stdout.on('data', (data) => {
|
|
245
|
+
stdout += data.toString();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
python.stderr.on('data', (data) => {
|
|
249
|
+
stderr += data.toString();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
python.on('close', (code) => {
|
|
253
|
+
clearTimeout(timer);
|
|
254
|
+
|
|
255
|
+
if (timedOut) return; // Already rejected
|
|
256
|
+
|
|
257
|
+
if (code !== 0) {
|
|
258
|
+
reject(new Error(`Python exited with code ${code}: ${stderr}`));
|
|
259
|
+
} else {
|
|
260
|
+
resolve(stdout);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
python.on('error', (err) => {
|
|
265
|
+
clearTimeout(timer);
|
|
266
|
+
reject(new Error(`Failed to start Python: ${err.message}`));
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function parseTimeout(timeout: string): number {
|
|
272
|
+
const match = timeout.match(/^(\d+)(s|m|h)?$/);
|
|
273
|
+
if (!match) return 30000; // default 30s
|
|
274
|
+
|
|
275
|
+
const value = parseInt(match[1]);
|
|
276
|
+
const unit = match[2] || 's';
|
|
277
|
+
|
|
278
|
+
switch (unit) {
|
|
279
|
+
case 's': return value * 1000;
|
|
280
|
+
case 'm': return value * 60 * 1000;
|
|
281
|
+
case 'h': return value * 60 * 60 * 1000;
|
|
282
|
+
default: return value * 1000;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Security & Sandboxing
|
|
288
|
+
|
|
289
|
+
**Baseline Security**:
|
|
290
|
+
```python
|
|
291
|
+
# 1. No network access (use Docker with --network=none)
|
|
292
|
+
# 2. Limited filesystem access (read-only mount)
|
|
293
|
+
# 3. Memory limits (Docker --memory flag)
|
|
294
|
+
# 4. CPU limits (Docker --cpus flag)
|
|
295
|
+
# 5. Timeout enforcement (killed after timeout)
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Docker-based Execution** (Production):
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
// worker/src/activities/python-interpreter.ts (production version)
|
|
302
|
+
import Docker from 'dockerode';
|
|
303
|
+
|
|
304
|
+
const docker = new Docker();
|
|
305
|
+
|
|
306
|
+
export async function executePython(
|
|
307
|
+
script: string,
|
|
308
|
+
context: Record<string, any>,
|
|
309
|
+
options: { timeout: string }
|
|
310
|
+
): Promise<any> {
|
|
311
|
+
const scriptId = uuid();
|
|
312
|
+
|
|
313
|
+
// Create container
|
|
314
|
+
const container = await docker.createContainer({
|
|
315
|
+
Image: 'python:3.11-slim',
|
|
316
|
+
Cmd: ['python', '-c', buildScript(script, context)],
|
|
317
|
+
HostConfig: {
|
|
318
|
+
Memory: 512 * 1024 * 1024, // 512MB
|
|
319
|
+
MemorySwap: 512 * 1024 * 1024,
|
|
320
|
+
NanoCpus: 1 * 1e9, // 1 CPU
|
|
321
|
+
NetworkMode: 'none', // No network access
|
|
322
|
+
ReadonlyRootfs: true, // Read-only filesystem
|
|
323
|
+
AutoRemove: true,
|
|
324
|
+
},
|
|
325
|
+
WorkingDir: '/workspace',
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
// Start container
|
|
330
|
+
await container.start();
|
|
331
|
+
|
|
332
|
+
// Wait for completion with timeout
|
|
333
|
+
const timeoutMs = parseTimeout(options.timeout);
|
|
334
|
+
const result = await Promise.race([
|
|
335
|
+
container.wait(),
|
|
336
|
+
new Promise((_, reject) => {
|
|
337
|
+
setTimeout(() => {
|
|
338
|
+
container.kill();
|
|
339
|
+
reject(new Error('Timeout'));
|
|
340
|
+
}, timeoutMs);
|
|
341
|
+
}),
|
|
342
|
+
]);
|
|
343
|
+
|
|
344
|
+
// Get logs
|
|
345
|
+
const logs = await container.logs({
|
|
346
|
+
stdout: true,
|
|
347
|
+
stderr: true,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Parse output
|
|
351
|
+
return JSON.parse(logs.toString());
|
|
352
|
+
} finally {
|
|
353
|
+
// Container auto-removed due to AutoRemove: true
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
**Allowed Python Packages** (pre-installed in Docker image):
|
|
359
|
+
- numpy, pandas, scipy (data processing)
|
|
360
|
+
- requests (HTTP - but network disabled, just for lib compatibility)
|
|
361
|
+
- json, datetime, math (stdlib)
|
|
362
|
+
- No database drivers, no subprocess, no file I/O
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## Phase 2B: Custom Node Uploads (Week 7-8)
|
|
367
|
+
|
|
368
|
+
### Architecture
|
|
369
|
+
|
|
370
|
+
```
|
|
371
|
+
User writes TypeScript node
|
|
372
|
+
↓
|
|
373
|
+
Uploads to platform (POST /api/custom-nodes)
|
|
374
|
+
↓
|
|
375
|
+
Server validates and compiles with esbuild
|
|
376
|
+
↓
|
|
377
|
+
Bundle stored in GCS
|
|
378
|
+
↓
|
|
379
|
+
Metadata stored in PostgreSQL
|
|
380
|
+
↓
|
|
381
|
+
Workers dynamically import bundle
|
|
382
|
+
↓
|
|
383
|
+
Node available in workflows
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Database Schema
|
|
387
|
+
|
|
388
|
+
```sql
|
|
389
|
+
CREATE TABLE custom_nodes (
|
|
390
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
391
|
+
tenant_id UUID REFERENCES tenants(id) NOT NULL,
|
|
392
|
+
name VARCHAR(255) NOT NULL, -- e.g., "SendSlackMessage"
|
|
393
|
+
version VARCHAR(20) NOT NULL, -- e.g., "1.0.0"
|
|
394
|
+
description TEXT,
|
|
395
|
+
source_code TEXT NOT NULL, -- TypeScript source
|
|
396
|
+
bundle_url VARCHAR(500), -- GCS URL
|
|
397
|
+
schema JSONB, -- Zod schema for props validation
|
|
398
|
+
status VARCHAR(50) DEFAULT 'draft', -- draft, active, deprecated
|
|
399
|
+
created_by UUID REFERENCES users(id),
|
|
400
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
401
|
+
|
|
402
|
+
UNIQUE(tenant_id, name, version)
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
CREATE TABLE workflow_node_dependencies (
|
|
406
|
+
workflow_id UUID REFERENCES workflows(id),
|
|
407
|
+
custom_node_id UUID REFERENCES custom_nodes(id),
|
|
408
|
+
|
|
409
|
+
PRIMARY KEY(workflow_id, custom_node_id)
|
|
410
|
+
);
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Upload API
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
// api-server/src/routes/custom-nodes.ts
|
|
417
|
+
import { FastifyInstance } from 'fastify';
|
|
418
|
+
import * as esbuild from 'esbuild';
|
|
419
|
+
import { Storage } from '@google-cloud/storage';
|
|
420
|
+
import { z } from 'zod';
|
|
421
|
+
|
|
422
|
+
const storage = new Storage();
|
|
423
|
+
const bucket = storage.bucket(process.env.GCS_BUCKET!);
|
|
424
|
+
|
|
425
|
+
const uploadSchema = z.object({
|
|
426
|
+
name: z.string().min(1).max(255),
|
|
427
|
+
version: z.string().regex(/^\d+\.\d+\.\d+$/), // semver
|
|
428
|
+
description: z.string().optional(),
|
|
429
|
+
sourceCode: z.string().min(1),
|
|
430
|
+
schema: z.record(z.any()).optional(), // Zod schema as JSON
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
export default async (fastify: FastifyInstance) => {
|
|
434
|
+
// Upload custom node
|
|
435
|
+
fastify.post('/custom-nodes', {
|
|
436
|
+
onRequest: [fastify.authenticate],
|
|
437
|
+
schema: { body: uploadSchema },
|
|
438
|
+
}, async (request) => {
|
|
439
|
+
const { name, version, description, sourceCode, schema } = request.body;
|
|
440
|
+
const { tenantId } = request;
|
|
441
|
+
|
|
442
|
+
// 1. Validate TypeScript syntax
|
|
443
|
+
try {
|
|
444
|
+
await esbuild.transform(sourceCode, {
|
|
445
|
+
loader: 'ts',
|
|
446
|
+
target: 'es2020',
|
|
447
|
+
});
|
|
448
|
+
} catch (error) {
|
|
449
|
+
throw fastify.httpErrors.badRequest(
|
|
450
|
+
`TypeScript compilation failed: ${error.message}`
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// 2. Bundle the code
|
|
455
|
+
const bundleResult = await esbuild.build({
|
|
456
|
+
stdin: {
|
|
457
|
+
contents: sourceCode,
|
|
458
|
+
loader: 'ts',
|
|
459
|
+
resolveDir: process.cwd(),
|
|
460
|
+
},
|
|
461
|
+
bundle: true,
|
|
462
|
+
platform: 'node',
|
|
463
|
+
target: 'node18',
|
|
464
|
+
format: 'esm',
|
|
465
|
+
external: ['@wayfarer-ai/btree', '@temporalio/workflow'],
|
|
466
|
+
write: false,
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const bundleCode = bundleResult.outputFiles[0].text;
|
|
470
|
+
|
|
471
|
+
// 3. Upload bundle to GCS
|
|
472
|
+
const bundlePath = `${tenantId}/custom-nodes/${name}/${version}/bundle.js`;
|
|
473
|
+
const file = bucket.file(bundlePath);
|
|
474
|
+
|
|
475
|
+
await file.save(bundleCode, {
|
|
476
|
+
contentType: 'application/javascript',
|
|
477
|
+
metadata: {
|
|
478
|
+
tenantId,
|
|
479
|
+
nodeName: name,
|
|
480
|
+
version,
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
const bundleUrl = `gs://${process.env.GCS_BUCKET}/${bundlePath}`;
|
|
485
|
+
|
|
486
|
+
// 4. Save metadata to database
|
|
487
|
+
const [customNode] = await fastify.db
|
|
488
|
+
.insert(schema.customNodes)
|
|
489
|
+
.values({
|
|
490
|
+
tenantId,
|
|
491
|
+
name,
|
|
492
|
+
version,
|
|
493
|
+
description,
|
|
494
|
+
sourceCode,
|
|
495
|
+
bundleUrl,
|
|
496
|
+
schema,
|
|
497
|
+
status: 'draft',
|
|
498
|
+
createdBy: request.user.id,
|
|
499
|
+
})
|
|
500
|
+
.returning();
|
|
501
|
+
|
|
502
|
+
return { customNode };
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// List custom nodes
|
|
506
|
+
fastify.get('/custom-nodes', {
|
|
507
|
+
onRequest: [fastify.authenticate],
|
|
508
|
+
}, async (request) => {
|
|
509
|
+
const { tenantId } = request;
|
|
510
|
+
|
|
511
|
+
const nodes = await fastify.db
|
|
512
|
+
.select()
|
|
513
|
+
.from(schema.customNodes)
|
|
514
|
+
.where(eq(schema.customNodes.tenantId, tenantId))
|
|
515
|
+
.orderBy(desc(schema.customNodes.createdAt));
|
|
516
|
+
|
|
517
|
+
return { nodes };
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// Activate custom node (make available in workflows)
|
|
521
|
+
fastify.post('/custom-nodes/:id/activate', {
|
|
522
|
+
onRequest: [fastify.authenticate],
|
|
523
|
+
}, async (request) => {
|
|
524
|
+
const { id } = request.params;
|
|
525
|
+
const { tenantId } = request;
|
|
526
|
+
|
|
527
|
+
const [node] = await fastify.db
|
|
528
|
+
.update(schema.customNodes)
|
|
529
|
+
.set({ status: 'active' })
|
|
530
|
+
.where(and(
|
|
531
|
+
eq(schema.customNodes.id, id),
|
|
532
|
+
eq(schema.customNodes.tenantId, tenantId)
|
|
533
|
+
))
|
|
534
|
+
.returning();
|
|
535
|
+
|
|
536
|
+
return { node, message: 'Custom node activated' };
|
|
537
|
+
});
|
|
538
|
+
};
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
### Worker Integration
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
// worker/src/custom-node-loader.ts
|
|
545
|
+
import { Storage } from '@google-cloud/storage';
|
|
546
|
+
import { writeFileSync, readFileSync } from 'fs';
|
|
547
|
+
import { join } from 'path';
|
|
548
|
+
|
|
549
|
+
const storage = new Storage();
|
|
550
|
+
const bucket = storage.bucket(process.env.GCS_BUCKET!);
|
|
551
|
+
|
|
552
|
+
export class CustomNodeLoader {
|
|
553
|
+
private cache = new Map<string, any>();
|
|
554
|
+
|
|
555
|
+
async loadCustomNode(
|
|
556
|
+
tenantId: string,
|
|
557
|
+
nodeName: string,
|
|
558
|
+
version: string
|
|
559
|
+
): Promise<any> {
|
|
560
|
+
const cacheKey = `${tenantId}:${nodeName}:${version}`;
|
|
561
|
+
|
|
562
|
+
// Check cache
|
|
563
|
+
if (this.cache.has(cacheKey)) {
|
|
564
|
+
return this.cache.get(cacheKey);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Download bundle from GCS
|
|
568
|
+
const bundlePath = `${tenantId}/custom-nodes/${nodeName}/${version}/bundle.js`;
|
|
569
|
+
const localPath = join('/tmp', 'custom-nodes', bundlePath);
|
|
570
|
+
|
|
571
|
+
const file = bucket.file(bundlePath);
|
|
572
|
+
await file.download({ destination: localPath });
|
|
573
|
+
|
|
574
|
+
// Dynamic import
|
|
575
|
+
const module = await import(localPath);
|
|
576
|
+
const NodeClass = module.default || module[nodeName];
|
|
577
|
+
|
|
578
|
+
if (!NodeClass) {
|
|
579
|
+
throw new Error(`Node class not found in bundle: ${nodeName}`);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Cache for future use
|
|
583
|
+
this.cache.set(cacheKey, NodeClass);
|
|
584
|
+
|
|
585
|
+
return NodeClass;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
clearCache() {
|
|
589
|
+
this.cache.clear();
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
```typescript
|
|
595
|
+
// worker/src/workflows/yaml-workflow.ts
|
|
596
|
+
import {
|
|
597
|
+
BehaviorTree,
|
|
598
|
+
Registry,
|
|
599
|
+
registerStandardNodes,
|
|
600
|
+
loadTreeFromYaml,
|
|
601
|
+
} from '@wayfarer-ai/btree';
|
|
602
|
+
import { CustomNodeLoader } from '../custom-node-loader';
|
|
603
|
+
|
|
604
|
+
const customNodeLoader = new CustomNodeLoader();
|
|
605
|
+
|
|
606
|
+
export async function yamlWorkflow(args: YamlWorkflowArgs) {
|
|
607
|
+
const { tenantId } = args.metadata;
|
|
608
|
+
|
|
609
|
+
// 1. Setup registry with standard nodes
|
|
610
|
+
const registry = new Registry();
|
|
611
|
+
registerStandardNodes(registry);
|
|
612
|
+
|
|
613
|
+
// 2. Load custom nodes for this tenant
|
|
614
|
+
const customNodes = await fetchCustomNodes(tenantId);
|
|
615
|
+
|
|
616
|
+
for (const node of customNodes) {
|
|
617
|
+
const NodeClass = await customNodeLoader.loadCustomNode(
|
|
618
|
+
tenantId,
|
|
619
|
+
node.name,
|
|
620
|
+
node.version
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
registry.register(node.name, NodeClass, {
|
|
624
|
+
category: 'action', // or from node metadata
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// 3. Parse YAML and execute
|
|
629
|
+
const root = loadTreeFromYaml(args.yamlContent, registry);
|
|
630
|
+
const tree = new BehaviorTree(root);
|
|
631
|
+
return tree.toWorkflow()(args);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async function fetchCustomNodes(tenantId: string) {
|
|
635
|
+
// Fetch from database (cached via worker startup)
|
|
636
|
+
// In production, cache this with TTL
|
|
637
|
+
return []; // TODO: implement
|
|
638
|
+
}
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### YAML Usage
|
|
642
|
+
|
|
643
|
+
Once uploaded and activated, use like built-in nodes:
|
|
644
|
+
|
|
645
|
+
```yaml
|
|
646
|
+
type: Sequence
|
|
647
|
+
id: notification-workflow
|
|
648
|
+
children:
|
|
649
|
+
# Custom node uploaded by tenant
|
|
650
|
+
- type: SendSlackMessage
|
|
651
|
+
id: notify-slack
|
|
652
|
+
props:
|
|
653
|
+
channel: "#alerts"
|
|
654
|
+
message: "Order {{orderId}} received!"
|
|
655
|
+
|
|
656
|
+
# Another custom node
|
|
657
|
+
- type: CreateJiraTicket
|
|
658
|
+
id: create-ticket
|
|
659
|
+
props:
|
|
660
|
+
project: "OPS"
|
|
661
|
+
summary: "New order: {{orderId}}"
|
|
662
|
+
description: "Customer: {{customerEmail}}"
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### Example Custom Node Source
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
// User uploads this TypeScript code
|
|
669
|
+
import { ActionNode } from '@wayfarer-ai/btree';
|
|
670
|
+
import { proxyActivities } from '@temporalio/workflow';
|
|
671
|
+
import type { NodeStatus, TemporalContext } from '@wayfarer-ai/btree';
|
|
672
|
+
|
|
673
|
+
interface SlackActivities {
|
|
674
|
+
sendSlackMessage(channel: string, message: string): Promise<void>;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const activities = proxyActivities<SlackActivities>({
|
|
678
|
+
startToCloseTimeout: '30s',
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
export default class SendSlackMessage extends ActionNode {
|
|
682
|
+
private channel: string;
|
|
683
|
+
private message: string;
|
|
684
|
+
|
|
685
|
+
constructor(config: any) {
|
|
686
|
+
super(config);
|
|
687
|
+
this.channel = config.channel;
|
|
688
|
+
this.message = config.message;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
protected async executeTick(context: TemporalContext): Promise<NodeStatus> {
|
|
692
|
+
try {
|
|
693
|
+
// Resolve template variables
|
|
694
|
+
const resolvedMessage = this.resolveVariables(
|
|
695
|
+
this.message,
|
|
696
|
+
context.blackboard.toJSON()
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
// Call activity
|
|
700
|
+
await activities.sendSlackMessage(this.channel, resolvedMessage);
|
|
701
|
+
|
|
702
|
+
return NodeStatus.SUCCESS;
|
|
703
|
+
} catch (error) {
|
|
704
|
+
this._lastError = `Slack message failed: ${error.message}`;
|
|
705
|
+
return NodeStatus.FAILURE;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
private resolveVariables(template: string, context: Record<string, any>): string {
|
|
710
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
711
|
+
return context[key] || '';
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
### Security Considerations
|
|
718
|
+
|
|
719
|
+
**Code Review Required**:
|
|
720
|
+
- Manual approval before activation (Week 9 feature)
|
|
721
|
+
- Automated scanning for malicious patterns
|
|
722
|
+
- Rate limiting on custom node executions
|
|
723
|
+
|
|
724
|
+
**Isolation**:
|
|
725
|
+
- Per-tenant node registry (tenant A can't use tenant B's nodes)
|
|
726
|
+
- Separate GCS paths per tenant
|
|
727
|
+
- Worker cache isolation
|
|
728
|
+
|
|
729
|
+
**Versioning**:
|
|
730
|
+
- Immutable versions (can't modify v1.0.0 once deployed)
|
|
731
|
+
- Workflows pin to specific versions
|
|
732
|
+
- Deprecation warnings for old versions
|
|
733
|
+
|
|
734
|
+
---
|
|
735
|
+
|
|
736
|
+
## Phase 3: Full SDK (Week 9-12)
|
|
737
|
+
|
|
738
|
+
### Local Development
|
|
739
|
+
|
|
740
|
+
```bash
|
|
741
|
+
# Install SDK
|
|
742
|
+
npm install -g @wayfarer-ai/workflow-sdk
|
|
743
|
+
|
|
744
|
+
# Create new custom node
|
|
745
|
+
workflow-sdk init SendSlackMessage
|
|
746
|
+
|
|
747
|
+
# Generated template:
|
|
748
|
+
# src/
|
|
749
|
+
# send-slack-message.ts
|
|
750
|
+
# send-slack-message.test.ts
|
|
751
|
+
# send-slack-message.schema.ts
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
### CLI Commands
|
|
755
|
+
|
|
756
|
+
```bash
|
|
757
|
+
# Test locally
|
|
758
|
+
workflow-sdk test
|
|
759
|
+
|
|
760
|
+
# Validate
|
|
761
|
+
workflow-sdk validate
|
|
762
|
+
|
|
763
|
+
# Upload to platform
|
|
764
|
+
workflow-sdk deploy --tenant mycompany
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
### Marketplace
|
|
768
|
+
|
|
769
|
+
- Browse community nodes
|
|
770
|
+
- One-click install
|
|
771
|
+
- Reviews and ratings
|
|
772
|
+
- Verified publishers
|
|
773
|
+
|
|
774
|
+
---
|
|
775
|
+
|
|
776
|
+
## Comparison: Interpreter vs Custom Nodes
|
|
777
|
+
|
|
778
|
+
| Feature | Interpreter Nodes | Custom Node Uploads |
|
|
779
|
+
|---------|------------------|---------------------|
|
|
780
|
+
| **Ease of Use** | ✅ Very easy (paste code) | ⚠️ Requires TypeScript |
|
|
781
|
+
| **Performance** | ⚠️ Subprocess overhead | ✅ Native performance |
|
|
782
|
+
| **Type Safety** | ❌ No validation | ✅ Full TypeScript |
|
|
783
|
+
| **Reusability** | ❌ Copy-paste | ✅ Import in any workflow |
|
|
784
|
+
| **Versioning** | ❌ Manual | ✅ Semver |
|
|
785
|
+
| **Best For** | Quick scripts, data transforms | Reusable integrations, complex logic |
|
|
786
|
+
|
|
787
|
+
---
|
|
788
|
+
|
|
789
|
+
## Recommended Approach for MVP
|
|
790
|
+
|
|
791
|
+
**Week 1-4 (MVP)**:
|
|
792
|
+
- Ship with 32 built-in nodes
|
|
793
|
+
- Document Python/JavaScript interpreter pattern
|
|
794
|
+
- No custom uploads yet
|
|
795
|
+
|
|
796
|
+
**Week 5-6 (Phase 2A)**:
|
|
797
|
+
- Add PythonInterpreter and JavaScriptInterpreter nodes
|
|
798
|
+
- Sandboxed execution
|
|
799
|
+
- Allow inline scripts in YAML
|
|
800
|
+
|
|
801
|
+
**Week 7-8 (Phase 2B)**:
|
|
802
|
+
- Enable custom node uploads
|
|
803
|
+
- Basic UI for upload
|
|
804
|
+
- No marketplace yet
|
|
805
|
+
|
|
806
|
+
**Week 9-12 (Phase 3)**:
|
|
807
|
+
- Full SDK
|
|
808
|
+
- CLI tools
|
|
809
|
+
- Marketplace
|
|
810
|
+
|
|
811
|
+
This gives you a **production platform in 4 weeks** and custom nodes in **8 weeks**.
|
|
812
|
+
|
|
813
|
+
---
|
|
814
|
+
|
|
815
|
+
## Next Steps
|
|
816
|
+
|
|
817
|
+
1. **Decide on Phase 2A timeline**: Do you need interpreters for MVP, or can you wait until Week 5?
|
|
818
|
+
|
|
819
|
+
2. **Activity implementations**: Which integrations do you want built-in?
|
|
820
|
+
- Slack, Discord, email?
|
|
821
|
+
- Database (Postgres, MySQL, MongoDB)?
|
|
822
|
+
- AI (OpenAI, Anthropic)?
|
|
823
|
+
|
|
824
|
+
3. **Sandboxing approach**: Docker containers or simple subprocess?
|
|
825
|
+
|
|
826
|
+
Let me know and I can provide detailed implementation for any of these phases!
|