@db-lyon/flowkit 0.11.1 → 0.11.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/README.md +1 -1
- package/docs/ai-agents.md +368 -0
- package/docs/api-reference.md +430 -0
- package/docs/configuration.md +343 -0
- package/docs/custom-tasks.md +203 -0
- package/docs/getting-started.md +172 -0
- package/package.json +3 -2
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
Flowkit uses YAML files for declarative configuration, with support for layered merging, environment overlays, and schema validation via Zod.
|
|
4
|
+
|
|
5
|
+
## YAML schema
|
|
6
|
+
|
|
7
|
+
A flowkit config file has two top-level keys:
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
tasks:
|
|
11
|
+
# ...
|
|
12
|
+
flows:
|
|
13
|
+
# ...
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Both default to `{}` if omitted.
|
|
17
|
+
|
|
18
|
+
### Task definition
|
|
19
|
+
|
|
20
|
+
```yaml
|
|
21
|
+
tasks:
|
|
22
|
+
my_task:
|
|
23
|
+
class_path: path.to.MyTask # required — how to resolve the task class
|
|
24
|
+
description: What this task does # optional
|
|
25
|
+
group: etl # optional — logical grouping label
|
|
26
|
+
options: # optional — default options passed to the task
|
|
27
|
+
key: value
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| Field | Type | Required | Description |
|
|
31
|
+
|-------|------|----------|-------------|
|
|
32
|
+
| `class_path` | `string` | yes | Dotted path to the task class, or a registered name |
|
|
33
|
+
| `description` | `string` | no | Human-readable description |
|
|
34
|
+
| `group` | `string` | no | Logical grouping label |
|
|
35
|
+
| `options` | `object` | no | Default options (merged with step-level overrides) |
|
|
36
|
+
|
|
37
|
+
### Flow definition
|
|
38
|
+
|
|
39
|
+
```yaml
|
|
40
|
+
flows:
|
|
41
|
+
my_flow:
|
|
42
|
+
description: What this flow does # required
|
|
43
|
+
steps:
|
|
44
|
+
1:
|
|
45
|
+
task: my_task # reference a task by name
|
|
46
|
+
options: # optional — override/extend task defaults
|
|
47
|
+
key: override_value
|
|
48
|
+
2:
|
|
49
|
+
flow: other_flow # reference another flow (nesting)
|
|
50
|
+
3:
|
|
51
|
+
task: None # skip sentinel — step is always skipped
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
| Field | Type | Required | Description |
|
|
55
|
+
|-------|------|----------|-------------|
|
|
56
|
+
| `description` | `string` | yes | Human-readable flow description |
|
|
57
|
+
| `steps` | `object` | yes | Steps keyed by number (execution order) |
|
|
58
|
+
|
|
59
|
+
### Flow step
|
|
60
|
+
|
|
61
|
+
Each step must have exactly one of `task` or `flow` (mutually exclusive), unless `task: None` is used to mark a skipped step.
|
|
62
|
+
|
|
63
|
+
| Field | Type | Required | Description |
|
|
64
|
+
|-------|------|----------|-------------|
|
|
65
|
+
| `task` | `string` | one of task/flow | Task name to execute |
|
|
66
|
+
| `flow` | `string` | one of task/flow | Nested flow name to execute |
|
|
67
|
+
| `options` | `object` | no | Override options for this step |
|
|
68
|
+
|
|
69
|
+
Step numbers are sorted numerically at execution time, so `1, 2, 10` runs in that order (not lexicographic `1, 10, 2`).
|
|
70
|
+
|
|
71
|
+
### Options merging
|
|
72
|
+
|
|
73
|
+
When a step executes, options are merged as: **task defaults** + **step overrides** (step wins):
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
tasks:
|
|
77
|
+
deploy:
|
|
78
|
+
class_path: tasks.Deploy
|
|
79
|
+
options:
|
|
80
|
+
environment: staging
|
|
81
|
+
notify: true
|
|
82
|
+
|
|
83
|
+
flows:
|
|
84
|
+
release:
|
|
85
|
+
description: Deploy to production
|
|
86
|
+
steps:
|
|
87
|
+
1:
|
|
88
|
+
task: deploy
|
|
89
|
+
options:
|
|
90
|
+
environment: production # overrides "staging"
|
|
91
|
+
# notify: true is inherited from task defaults
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Runtime parameters passed to `FlowRunner.run({ params })` merge on top with the highest priority (**task defaults < step overrides < runtime params**).
|
|
95
|
+
|
|
96
|
+
### Step references
|
|
97
|
+
|
|
98
|
+
Option values may reference the output of earlier steps in the same flow using `${steps.<id>.<path>}`:
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
flows:
|
|
102
|
+
chain:
|
|
103
|
+
description: Pass one step's output into the next
|
|
104
|
+
steps:
|
|
105
|
+
1:
|
|
106
|
+
task: build
|
|
107
|
+
options:
|
|
108
|
+
target: plugin
|
|
109
|
+
2:
|
|
110
|
+
task: deploy
|
|
111
|
+
options:
|
|
112
|
+
artifact: ${steps.1.path} # whole-value → raw type preserved
|
|
113
|
+
message: "deployed ${steps.build.version}" # embedded → stringified
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
- **`<id>`** is a step number (`1`) or a task name (`build`, `level.place_actor`). Task names with dots are matched longest-prefix-first.
|
|
117
|
+
- **`<path>`** is a dot path into the step's `result.data`.
|
|
118
|
+
- When a task name appears in multiple steps, references resolve to the **most recently completed** one.
|
|
119
|
+
- A reference that fills the entire string (`"${steps.1.path}"`) is replaced with the raw value, so objects and arrays round-trip. References embedded inside a larger string are stringified.
|
|
120
|
+
- References that can't be resolved throw and fail the step.
|
|
121
|
+
|
|
122
|
+
References resolve just before the step runs, against the results of already-completed steps in the current flow. Nested flows have their own reference scope — they don't see their parent flow's steps.
|
|
123
|
+
|
|
124
|
+
### Flow-level hooks
|
|
125
|
+
|
|
126
|
+
A flow can attach steps that run around the main step sequence, keyed by flow outcome:
|
|
127
|
+
|
|
128
|
+
```yaml
|
|
129
|
+
flows:
|
|
130
|
+
deploy:
|
|
131
|
+
description: Deploy to prod
|
|
132
|
+
on_start: [ { task: notify, options: { msg: "starting" } } ]
|
|
133
|
+
on_success: [ { task: notify, options: { msg: "done ${steps.build.version}" } } ]
|
|
134
|
+
on_failure: [ { task: notify, options: { msg: "failed: ${error.message}" } } ]
|
|
135
|
+
finally: [ { task: cleanup } ]
|
|
136
|
+
steps:
|
|
137
|
+
1: { task: build }
|
|
138
|
+
2: { task: push }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
- **`on_start`** runs before any step. Its failure aborts the flow before steps execute.
|
|
142
|
+
- **`on_success`** runs when all steps succeed.
|
|
143
|
+
- **`on_failure`** runs when any step fails. It can reference the error via the `${error.*}` namespace.
|
|
144
|
+
- **`finally`** runs after either outcome, after `on_success`/`on_failure`.
|
|
145
|
+
|
|
146
|
+
Hook steps share the full step execution model — same task dispatch, same option merging, same runtime params, same `${steps.X.y}` resolution. Inside `on_failure` and `finally`, the `${error.message}`, `${error.name}`, `${error.stack}`, and `${error.step}` references resolve to the failure that triggered them.
|
|
147
|
+
|
|
148
|
+
Hook failures are captured in `FlowRunResult.hookErrors` but **do not** change the flow's primary success/failure outcome — a failed notifier doesn't rewrite history.
|
|
149
|
+
|
|
150
|
+
### Per-step retry
|
|
151
|
+
|
|
152
|
+
A step can retry itself on failure:
|
|
153
|
+
|
|
154
|
+
```yaml
|
|
155
|
+
steps:
|
|
156
|
+
1:
|
|
157
|
+
task: flaky_network_call
|
|
158
|
+
retries: 3 # up to 4 total attempts
|
|
159
|
+
retryDelay: 500 # ms between attempts
|
|
160
|
+
retryOn: "timeout" # only retry when the error message contains this substring
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Omit `retryOn` to retry on any error. The number of attempts taken appears on `FlowStepResult.attempts`.
|
|
164
|
+
|
|
165
|
+
### Rollback on failure
|
|
166
|
+
|
|
167
|
+
Mutating tasks may return a `rollback` record on their `TaskResult` pointing to an inverse task:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
return {
|
|
171
|
+
success: true,
|
|
172
|
+
data: { label: 'MyPillar' },
|
|
173
|
+
rollback: { taskName: 'delete_actor', payload: { label: 'MyPillar' } },
|
|
174
|
+
};
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
When a flow sets `rollback_on_failure: true` (or the caller passes it on `FlowRunRunOptions`) and a later step fails, the runner invokes the collected rollback records in **reverse order**, best-effort: it continues past individual failures and reports all errors in `FlowRunResult.rollback`.
|
|
178
|
+
|
|
179
|
+
```yaml
|
|
180
|
+
flows:
|
|
181
|
+
safe_deploy:
|
|
182
|
+
description: Deploy with rollback on failure
|
|
183
|
+
rollback_on_failure: true
|
|
184
|
+
steps:
|
|
185
|
+
1: { task: create_thing, options: { label: A } }
|
|
186
|
+
2: { task: create_thing, options: { label: B } }
|
|
187
|
+
3: { task: finalize } # if this fails, thing:B then thing:A are rolled back
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Rollback runs after `on_failure` and before `finally`. Nested flow steps' rollback records bubble up to the parent flow so a single `rollback_on_failure` setting covers the whole tree.
|
|
191
|
+
|
|
192
|
+
### `agent_prompt` — LLM step
|
|
193
|
+
|
|
194
|
+
When a `LLMProvider` is attached to the context under `ctx.llm`, the built-in `agent_prompt` task invokes it:
|
|
195
|
+
|
|
196
|
+
```yaml
|
|
197
|
+
steps:
|
|
198
|
+
1:
|
|
199
|
+
task: agent_prompt
|
|
200
|
+
options:
|
|
201
|
+
system: "You are a deployment triage agent."
|
|
202
|
+
prompt: "Last error: ${error.message}. Suggest a fix."
|
|
203
|
+
model: claude-opus-4-6
|
|
204
|
+
maxTokens: 512
|
|
205
|
+
schema: { type: object, properties: { fix: { type: string } } } # optional
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Returns `{ text, parsed?, usage? }`. Provider failures become step failures; missing provider is a clear error.
|
|
209
|
+
|
|
210
|
+
## Config layering
|
|
211
|
+
|
|
212
|
+
`loadConfig()` merges up to four layers, left to right:
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
defaults (code) → base file → env overlay → local overlay
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
| Layer | Source | Purpose |
|
|
219
|
+
|-------|--------|---------|
|
|
220
|
+
| 1. Defaults | `options.defaults` in code | Hardcoded fallbacks |
|
|
221
|
+
| 2. Base file | `pipeline.yml` | Project-level config (committed) |
|
|
222
|
+
| 3. Env overlay | `pipeline.staging.yml` | Environment-specific overrides |
|
|
223
|
+
| 4. Local overlay | `pipeline.local.yml` | Developer-specific overrides (gitignored) |
|
|
224
|
+
|
|
225
|
+
### Example
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
import { loadConfig, EngineConfigSchema } from '@db-lyon/flowkit';
|
|
229
|
+
|
|
230
|
+
const { config, configDir } = loadConfig({
|
|
231
|
+
filename: 'pipeline.yml',
|
|
232
|
+
schema: EngineConfigSchema,
|
|
233
|
+
|
|
234
|
+
// Hardcoded defaults merged under everything
|
|
235
|
+
defaults: {
|
|
236
|
+
tasks: {},
|
|
237
|
+
flows: {},
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
// Environment name — loads pipeline.{env}.yml
|
|
241
|
+
env: process.env.NODE_ENV,
|
|
242
|
+
// Or read from a specific env var:
|
|
243
|
+
// envVar: 'APP_ENV',
|
|
244
|
+
|
|
245
|
+
// Directory to search (default: cwd)
|
|
246
|
+
configDir: './config',
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The `configDir` return value tells you where the config was loaded from.
|
|
251
|
+
|
|
252
|
+
### Environment selection
|
|
253
|
+
|
|
254
|
+
You can specify the environment explicitly or via an env var:
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
// Explicit
|
|
258
|
+
loadConfig({ filename: 'app.yml', schema, env: 'production' });
|
|
259
|
+
|
|
260
|
+
// From env var — reads process.env.APP_ENV
|
|
261
|
+
loadConfig({ filename: 'app.yml', schema, envVar: 'APP_ENV' });
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
If both `env` and `envVar` are provided, `env` takes precedence.
|
|
265
|
+
|
|
266
|
+
## Deep merge behavior
|
|
267
|
+
|
|
268
|
+
Config layers are merged using `deepMerge()`, which follows these rules:
|
|
269
|
+
|
|
270
|
+
| Scenario | Behavior |
|
|
271
|
+
|----------|----------|
|
|
272
|
+
| Objects | Recursive key-by-key merge (override wins per-key) |
|
|
273
|
+
| Arrays | Override replaces the base array |
|
|
274
|
+
| Scalars | Override wins |
|
|
275
|
+
| `null` override | Explicitly nullifies the base value |
|
|
276
|
+
| `undefined` override | No-op (base preserved) |
|
|
277
|
+
|
|
278
|
+
### Array append mode
|
|
279
|
+
|
|
280
|
+
By default, arrays in an overlay replace the base array entirely. To append instead, add `__merge: append` to the override array:
|
|
281
|
+
|
|
282
|
+
```yaml
|
|
283
|
+
# base.yml
|
|
284
|
+
plugins:
|
|
285
|
+
- eslint
|
|
286
|
+
- prettier
|
|
287
|
+
|
|
288
|
+
# base.local.yml
|
|
289
|
+
plugins:
|
|
290
|
+
- __merge: append
|
|
291
|
+
- my-custom-plugin
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Result: `['eslint', 'prettier', 'my-custom-plugin']`
|
|
295
|
+
|
|
296
|
+
The `__merge` annotation is stripped from the final array.
|
|
297
|
+
|
|
298
|
+
## Finding config files
|
|
299
|
+
|
|
300
|
+
`findConfigFile()` walks up parent directories to locate a file:
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
import { findConfigFile } from '@db-lyon/flowkit';
|
|
304
|
+
|
|
305
|
+
const path = findConfigFile('pipeline.yml');
|
|
306
|
+
// Searches cwd, then parent, then grandparent, etc.
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Throws if the file isn't found in any ancestor directory.
|
|
310
|
+
|
|
311
|
+
## Loading raw YAML
|
|
312
|
+
|
|
313
|
+
For cases where you need the raw parsed YAML without schema validation:
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
import { loadRawYaml } from '@db-lyon/flowkit';
|
|
317
|
+
|
|
318
|
+
const data = loadRawYaml('/path/to/file.yml');
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Custom schemas
|
|
322
|
+
|
|
323
|
+
`EngineConfigSchema` is the minimal schema flowkit needs. You can extend it for your own config sections:
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
import { z } from 'zod';
|
|
327
|
+
import { EngineConfigSchema } from '@db-lyon/flowkit';
|
|
328
|
+
|
|
329
|
+
const AppConfigSchema = EngineConfigSchema.extend({
|
|
330
|
+
database: z.object({
|
|
331
|
+
host: z.string(),
|
|
332
|
+
port: z.number().default(5432),
|
|
333
|
+
}),
|
|
334
|
+
features: z.record(z.boolean()).default({}),
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const { config } = loadConfig({
|
|
338
|
+
filename: 'app.yml',
|
|
339
|
+
schema: AppConfigSchema,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// config.tasks, config.flows, config.database, config.features
|
|
343
|
+
```
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Custom tasks
|
|
2
|
+
|
|
3
|
+
Tasks are the building blocks of flowkit. Each task is a class that extends `BaseTask` and implements an `execute()` method.
|
|
4
|
+
|
|
5
|
+
## Anatomy of a task
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { BaseTask, type TaskResult } from '@db-lyon/flowkit';
|
|
9
|
+
|
|
10
|
+
interface MyOptions {
|
|
11
|
+
url: string;
|
|
12
|
+
retries?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default class FetchData extends BaseTask<MyOptions> {
|
|
16
|
+
get taskName() {
|
|
17
|
+
return 'fetch_data';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
protected validate() {
|
|
21
|
+
if (!this.options.url) {
|
|
22
|
+
throw new Error('url option is required');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async execute(): Promise<TaskResult> {
|
|
27
|
+
const { url, retries = 3 } = this.options;
|
|
28
|
+
|
|
29
|
+
const response = await fetch(url);
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
return {
|
|
32
|
+
success: false,
|
|
33
|
+
error: new Error(`HTTP ${response.status}`),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const data = await response.json();
|
|
38
|
+
return {
|
|
39
|
+
success: true,
|
|
40
|
+
data: { body: data, status: response.status },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Required members
|
|
47
|
+
|
|
48
|
+
| Member | Description |
|
|
49
|
+
|--------|-------------|
|
|
50
|
+
| `get taskName()` | A human-readable name used in logging |
|
|
51
|
+
| `execute()` | Async method that performs the work and returns a `TaskResult` |
|
|
52
|
+
|
|
53
|
+
### Optional members
|
|
54
|
+
|
|
55
|
+
| Member | Description |
|
|
56
|
+
|--------|-------------|
|
|
57
|
+
| `validate()` | Called before `execute()`. Throw to abort with a validation error. |
|
|
58
|
+
|
|
59
|
+
### Available on `this`
|
|
60
|
+
|
|
61
|
+
| Property | Description |
|
|
62
|
+
|----------|-------------|
|
|
63
|
+
| `this.options` | The merged options (task defaults + step overrides), typed as `TOptions` |
|
|
64
|
+
| `this.ctx` | The `TaskContext` passed to the flow runner — use it to share state |
|
|
65
|
+
| `this.logger` | A child logger scoped to this task instance |
|
|
66
|
+
|
|
67
|
+
## The task lifecycle
|
|
68
|
+
|
|
69
|
+
When `task.run()` is called (by the flow runner):
|
|
70
|
+
|
|
71
|
+
1. `validate()` runs — throw here to reject bad options
|
|
72
|
+
2. `execute()` runs — return a `TaskResult`
|
|
73
|
+
3. The result gets a `duration` field added automatically
|
|
74
|
+
4. If `validate()` or `execute()` throws, the error is caught and returned as `{ success: false, error }`
|
|
75
|
+
|
|
76
|
+
You never call `run()` yourself in normal usage — the flow runner handles it.
|
|
77
|
+
|
|
78
|
+
## TaskResult
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
interface TaskResult {
|
|
82
|
+
success: boolean;
|
|
83
|
+
data?: Record<string, unknown>; // arbitrary output data
|
|
84
|
+
error?: Error; // populated on failure
|
|
85
|
+
duration?: number; // milliseconds, set by run()
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Return `{ success: true }` for success and `{ success: false, error }` for expected failures. Unexpected exceptions are caught automatically.
|
|
90
|
+
|
|
91
|
+
## TaskContext
|
|
92
|
+
|
|
93
|
+
The context object is shared across all tasks in a flow run. Use it to pass shared state like database connections, API clients, or configuration:
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
const runner = new FlowRunner({
|
|
97
|
+
// ...
|
|
98
|
+
context: {
|
|
99
|
+
logger: myLogger,
|
|
100
|
+
db: databaseConnection,
|
|
101
|
+
apiKey: process.env.API_KEY,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Inside a task:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
async execute(): Promise<TaskResult> {
|
|
110
|
+
const db = this.ctx.db as Database;
|
|
111
|
+
// ...
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Registering tasks
|
|
116
|
+
|
|
117
|
+
### By name
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
const registry = new TaskRegistry();
|
|
121
|
+
registry.register('fetch_data', FetchData as any);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The YAML can then reference it directly:
|
|
125
|
+
|
|
126
|
+
```yaml
|
|
127
|
+
tasks:
|
|
128
|
+
fetch_data:
|
|
129
|
+
class_path: fetch_data
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### By class path
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
registry.registerClassPath('my.tasks.FetchData', FetchData as any);
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Bulk registration
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
registry.registerAll({
|
|
142
|
+
fetch_data: FetchData as any,
|
|
143
|
+
transform: TransformData as any,
|
|
144
|
+
upload: Upload as any,
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Dynamic resolution
|
|
149
|
+
|
|
150
|
+
If a `class_path` isn't found in the registry, flowkit converts dots to path separators and looks for a file on disk:
|
|
151
|
+
|
|
152
|
+
| class_path | Files checked |
|
|
153
|
+
|------------|---------------|
|
|
154
|
+
| `tasks.FetchData` | `tasks/FetchData.ts`, `tasks/FetchData.js`, `tasks/FetchData/index.ts`, `tasks/FetchData/index.js` |
|
|
155
|
+
| `lib.etl.Extract` | `lib/etl/Extract.ts`, `lib/etl/Extract.js`, ... |
|
|
156
|
+
|
|
157
|
+
The module must have either a `default` export or a named export matching the last segment of the path (e.g., `FetchData`). The export must extend `BaseTask`.
|
|
158
|
+
|
|
159
|
+
## Built-in: ShellTask
|
|
160
|
+
|
|
161
|
+
`ShellTask` executes shell commands via `execSync`. Register it under any name you like:
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
import { ShellTask } from '@db-lyon/flowkit';
|
|
165
|
+
|
|
166
|
+
registry.register('shell', ShellTask as any);
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Then use it in YAML:
|
|
170
|
+
|
|
171
|
+
```yaml
|
|
172
|
+
tasks:
|
|
173
|
+
lint:
|
|
174
|
+
class_path: shell
|
|
175
|
+
description: Run the linter
|
|
176
|
+
options:
|
|
177
|
+
command: npm run lint
|
|
178
|
+
|
|
179
|
+
build:
|
|
180
|
+
class_path: shell
|
|
181
|
+
description: Build the project
|
|
182
|
+
options:
|
|
183
|
+
command: npm run build
|
|
184
|
+
cwd: /path/to/project
|
|
185
|
+
timeout: 120000
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### ShellTask options
|
|
189
|
+
|
|
190
|
+
| Option | Type | Default | Description |
|
|
191
|
+
|--------|------|---------|-------------|
|
|
192
|
+
| `command` | `string` | (required) | The shell command to execute |
|
|
193
|
+
| `cwd` | `string` | `undefined` | Working directory |
|
|
194
|
+
| `timeout` | `number` | `300000` (5 min) | Timeout in milliseconds |
|
|
195
|
+
|
|
196
|
+
On success, `result.data.output` contains the trimmed stdout. On failure, `result.data` includes `exitCode`, `stderr`, and `stdout`.
|
|
197
|
+
|
|
198
|
+
## Listing registered tasks
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
const names = registry.listRegistered();
|
|
202
|
+
// ['fetch_data', 'shell', 'my.tasks.Transform', ...]
|
|
203
|
+
```
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
This guide walks through setting up flowkit from scratch.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- Node.js >= 20
|
|
8
|
+
- TypeScript project with `"module": "NodeNext"` (or compatible ESM setup)
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @db-lyon/flowkit
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## 1. Create a YAML config
|
|
17
|
+
|
|
18
|
+
Create `config/pipeline.yml`:
|
|
19
|
+
|
|
20
|
+
```yaml
|
|
21
|
+
tasks:
|
|
22
|
+
greet:
|
|
23
|
+
class_path: tasks.Greet
|
|
24
|
+
description: Print a greeting
|
|
25
|
+
options:
|
|
26
|
+
name: world
|
|
27
|
+
|
|
28
|
+
flows:
|
|
29
|
+
hello:
|
|
30
|
+
description: Run the greeting
|
|
31
|
+
steps:
|
|
32
|
+
1:
|
|
33
|
+
task: greet
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### What's happening here
|
|
37
|
+
|
|
38
|
+
- **tasks** defines reusable units of work. Each task has a `class_path` that tells flowkit how to find the task implementation, and optional default `options`.
|
|
39
|
+
- **flows** defines sequences of steps. Each step references a task (or another flow) by name. Steps execute in numeric order.
|
|
40
|
+
|
|
41
|
+
## 2. Create a task class
|
|
42
|
+
|
|
43
|
+
Create `tasks/Greet.ts`:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { BaseTask, type TaskResult } from '@db-lyon/flowkit';
|
|
47
|
+
|
|
48
|
+
interface GreetOptions {
|
|
49
|
+
name: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default class Greet extends BaseTask<GreetOptions> {
|
|
53
|
+
get taskName() {
|
|
54
|
+
return 'greet';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async execute(): Promise<TaskResult> {
|
|
58
|
+
const message = `Hello, ${this.options.name}!`;
|
|
59
|
+
this.logger.info(message);
|
|
60
|
+
return { success: true, data: { message } };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Every task must:
|
|
66
|
+
1. Extend `BaseTask<TOptions>`
|
|
67
|
+
2. Implement the `taskName` getter
|
|
68
|
+
3. Implement the async `execute()` method returning a `TaskResult`
|
|
69
|
+
|
|
70
|
+
## 3. Wire it up
|
|
71
|
+
|
|
72
|
+
Create `run.ts`:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import {
|
|
76
|
+
loadConfig,
|
|
77
|
+
EngineConfigSchema,
|
|
78
|
+
TaskRegistry,
|
|
79
|
+
FlowRunner,
|
|
80
|
+
} from '@db-lyon/flowkit';
|
|
81
|
+
|
|
82
|
+
// Load and validate the YAML config
|
|
83
|
+
const { config } = loadConfig({
|
|
84
|
+
filename: 'pipeline.yml',
|
|
85
|
+
schema: EngineConfigSchema,
|
|
86
|
+
configDir: './config',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Create a registry — flowkit uses this to resolve class_path → constructor
|
|
90
|
+
const registry = new TaskRegistry();
|
|
91
|
+
|
|
92
|
+
// Create the flow runner
|
|
93
|
+
const runner = new FlowRunner({
|
|
94
|
+
tasks: config.tasks,
|
|
95
|
+
flows: config.flows,
|
|
96
|
+
registry,
|
|
97
|
+
context: {},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Run the flow
|
|
101
|
+
const result = await runner.run({ flowName: 'hello' });
|
|
102
|
+
|
|
103
|
+
if (result.success) {
|
|
104
|
+
console.log('Flow completed successfully');
|
|
105
|
+
} else {
|
|
106
|
+
console.error('Flow failed:', result.error?.message);
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## 4. Run it
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
npx tsx run.ts
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The `greet` task's `class_path: tasks.Greet` tells flowkit to look for `tasks/Greet.ts` (or `.js`) relative to `process.cwd()`. It dynamically imports the file and instantiates the default export.
|
|
117
|
+
|
|
118
|
+
## How task resolution works
|
|
119
|
+
|
|
120
|
+
When the flow runner encounters a task step, it:
|
|
121
|
+
|
|
122
|
+
1. Looks up the task name in `config.tasks` to get the `class_path`
|
|
123
|
+
2. Checks the registry for a constructor registered under that `class_path` or name
|
|
124
|
+
3. If not found, converts the dotted path to a file path and dynamically imports it (e.g., `tasks.Greet` → `tasks/Greet.ts`)
|
|
125
|
+
4. Instantiates the task with the merged options (task defaults + step overrides)
|
|
126
|
+
5. Calls `task.run()` which runs `validate()` → `execute()` → returns the result
|
|
127
|
+
|
|
128
|
+
## Explicit registration
|
|
129
|
+
|
|
130
|
+
Instead of relying on dynamic filesystem resolution, you can register tasks directly:
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import Greet from './tasks/Greet.js';
|
|
134
|
+
|
|
135
|
+
const registry = new TaskRegistry();
|
|
136
|
+
registry.register('greet', Greet as any);
|
|
137
|
+
|
|
138
|
+
// Or by class_path
|
|
139
|
+
registry.registerClassPath('tasks.Greet', Greet as any);
|
|
140
|
+
|
|
141
|
+
// Or bulk register
|
|
142
|
+
registry.registerAll({
|
|
143
|
+
greet: Greet as any,
|
|
144
|
+
// ...more tasks
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Adding a logger
|
|
149
|
+
|
|
150
|
+
Flowkit accepts any logger with `debug`, `info`, `warn`, `error`, and `child` methods (pino, winston, etc.):
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import pino from 'pino';
|
|
154
|
+
|
|
155
|
+
const logger = pino();
|
|
156
|
+
|
|
157
|
+
const runner = new FlowRunner({
|
|
158
|
+
tasks: config.tasks,
|
|
159
|
+
flows: config.flows,
|
|
160
|
+
registry,
|
|
161
|
+
context: { logger },
|
|
162
|
+
logger,
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The context logger is passed to each task instance. The runner logger is used for flow-level logging. Both fall back to a silent no-op logger if omitted.
|
|
167
|
+
|
|
168
|
+
## Next steps
|
|
169
|
+
|
|
170
|
+
- [Custom tasks](custom-tasks.md) — validation, error handling, and the ShellTask
|
|
171
|
+
- [Configuration](configuration.md) — layered configs, environment overlays, deep merge
|
|
172
|
+
- [API reference](api-reference.md) — full type and function docs
|