@db-lyon/flowkit 0.13.0 → 0.14.0
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 +387 -386
- package/dist/.tsbuildinfo +1 -1
- package/dist/guard/index.d.ts +7 -0
- package/dist/guard/index.d.ts.map +1 -0
- package/dist/guard/index.js +5 -0
- package/dist/guard/index.js.map +1 -0
- package/dist/guard/pipeline.d.ts +22 -0
- package/dist/guard/pipeline.d.ts.map +1 -0
- package/dist/guard/pipeline.js +45 -0
- package/dist/guard/pipeline.js.map +1 -0
- package/dist/guard/registry.d.ts +20 -0
- package/dist/guard/registry.d.ts.map +1 -0
- package/dist/guard/registry.js +33 -0
- package/dist/guard/registry.js.map +1 -0
- package/dist/guard/task-guards.d.ts +89 -0
- package/dist/guard/task-guards.d.ts.map +1 -0
- package/dist/guard/task-guards.js +83 -0
- package/dist/guard/task-guards.js.map +1 -0
- package/dist/guard/types.d.ts +49 -0
- package/dist/guard/types.d.ts.map +1 -0
- package/dist/guard/types.js +38 -0
- package/dist/guard/types.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/docs/ai-agents.md +368 -368
- package/docs/api-reference.md +568 -458
- package/docs/configuration.md +347 -347
- package/docs/custom-tasks.md +258 -258
- package/docs/guards.md +133 -0
- package/package.json +54 -53
- package/dist/flow/references.d.ts +0 -39
- package/dist/flow/references.d.ts.map +0 -1
- package/dist/flow/references.js +0 -102
- package/dist/flow/references.js.map +0 -1
package/docs/configuration.md
CHANGED
|
@@ -1,347 +1,347 @@
|
|
|
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
|
-
The inverse task's configured `options` are resolved for `${ns.path}` references as usual, then the `payload` is merged over them. A payload is runtime data the task recorded, not configuration, so it is passed through **literally** — a `${...}` captured inside one reaches the inverse task unchanged.
|
|
193
|
-
|
|
194
|
-
Rollback runs outside any step's scope, so those configured `options` resolve against the host namespaces only. A `${steps.…}` or `${error.…}` in an inverse task's defaults has nothing to resolve against and fails that one rollback record (it is reported in `rollback.errors`; the remaining records still run). Keep inverse-task defaults to host namespaces and put step-derived values in the `payload`.
|
|
195
|
-
|
|
196
|
-
### `agent_prompt` — LLM step
|
|
197
|
-
|
|
198
|
-
When a `LLMProvider` is attached to the context under `ctx.llm`, the built-in `agent_prompt` task invokes it:
|
|
199
|
-
|
|
200
|
-
```yaml
|
|
201
|
-
steps:
|
|
202
|
-
1:
|
|
203
|
-
task: agent_prompt
|
|
204
|
-
options:
|
|
205
|
-
system: "You are a deployment triage agent."
|
|
206
|
-
prompt: "Last error: ${error.message}. Suggest a fix."
|
|
207
|
-
model: claude-opus-4-6
|
|
208
|
-
maxTokens: 512
|
|
209
|
-
schema: { type: object, properties: { fix: { type: string } } } # optional
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
Returns `{ text, parsed?, usage? }`. Provider failures become step failures; missing provider is a clear error.
|
|
213
|
-
|
|
214
|
-
## Config layering
|
|
215
|
-
|
|
216
|
-
`loadConfig()` merges up to four layers, left to right:
|
|
217
|
-
|
|
218
|
-
```
|
|
219
|
-
defaults (code) → base file → env overlay → local overlay
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
| Layer | Source | Purpose |
|
|
223
|
-
|-------|--------|---------|
|
|
224
|
-
| 1. Defaults | `options.defaults` in code | Hardcoded fallbacks |
|
|
225
|
-
| 2. Base file | `pipeline.yml` | Project-level config (committed) |
|
|
226
|
-
| 3. Env overlay | `pipeline.staging.yml` | Environment-specific overrides |
|
|
227
|
-
| 4. Local overlay | `pipeline.local.yml` | Developer-specific overrides (gitignored) |
|
|
228
|
-
|
|
229
|
-
### Example
|
|
230
|
-
|
|
231
|
-
```typescript
|
|
232
|
-
import { loadConfig, EngineConfigSchema } from '@db-lyon/flowkit';
|
|
233
|
-
|
|
234
|
-
const { config, configDir } = loadConfig({
|
|
235
|
-
filename: 'pipeline.yml',
|
|
236
|
-
schema: EngineConfigSchema,
|
|
237
|
-
|
|
238
|
-
// Hardcoded defaults merged under everything
|
|
239
|
-
defaults: {
|
|
240
|
-
tasks: {},
|
|
241
|
-
flows: {},
|
|
242
|
-
},
|
|
243
|
-
|
|
244
|
-
// Environment name — loads pipeline.{env}.yml
|
|
245
|
-
env: process.env.NODE_ENV,
|
|
246
|
-
// Or read from a specific env var:
|
|
247
|
-
// envVar: 'APP_ENV',
|
|
248
|
-
|
|
249
|
-
// Directory to search (default: cwd)
|
|
250
|
-
configDir: './config',
|
|
251
|
-
});
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
The `configDir` return value tells you where the config was loaded from.
|
|
255
|
-
|
|
256
|
-
### Environment selection
|
|
257
|
-
|
|
258
|
-
You can specify the environment explicitly or via an env var:
|
|
259
|
-
|
|
260
|
-
```typescript
|
|
261
|
-
// Explicit
|
|
262
|
-
loadConfig({ filename: 'app.yml', schema, env: 'production' });
|
|
263
|
-
|
|
264
|
-
// From env var — reads process.env.APP_ENV
|
|
265
|
-
loadConfig({ filename: 'app.yml', schema, envVar: 'APP_ENV' });
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
If both `env` and `envVar` are provided, `env` takes precedence.
|
|
269
|
-
|
|
270
|
-
## Deep merge behavior
|
|
271
|
-
|
|
272
|
-
Config layers are merged using `deepMerge()`, which follows these rules:
|
|
273
|
-
|
|
274
|
-
| Scenario | Behavior |
|
|
275
|
-
|----------|----------|
|
|
276
|
-
| Objects | Recursive key-by-key merge (override wins per-key) |
|
|
277
|
-
| Arrays | Override replaces the base array |
|
|
278
|
-
| Scalars | Override wins |
|
|
279
|
-
| `null` override | Explicitly nullifies the base value |
|
|
280
|
-
| `undefined` override | No-op (base preserved) |
|
|
281
|
-
|
|
282
|
-
### Array append mode
|
|
283
|
-
|
|
284
|
-
By default, arrays in an overlay replace the base array entirely. To append instead, add `__merge: append` to the override array:
|
|
285
|
-
|
|
286
|
-
```yaml
|
|
287
|
-
# base.yml
|
|
288
|
-
plugins:
|
|
289
|
-
- eslint
|
|
290
|
-
- prettier
|
|
291
|
-
|
|
292
|
-
# base.local.yml
|
|
293
|
-
plugins:
|
|
294
|
-
- __merge: append
|
|
295
|
-
- my-custom-plugin
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
Result: `['eslint', 'prettier', 'my-custom-plugin']`
|
|
299
|
-
|
|
300
|
-
The `__merge` annotation is stripped from the final array.
|
|
301
|
-
|
|
302
|
-
## Finding config files
|
|
303
|
-
|
|
304
|
-
`findConfigFile()` walks up parent directories to locate a file:
|
|
305
|
-
|
|
306
|
-
```typescript
|
|
307
|
-
import { findConfigFile } from '@db-lyon/flowkit';
|
|
308
|
-
|
|
309
|
-
const path = findConfigFile('pipeline.yml');
|
|
310
|
-
// Searches cwd, then parent, then grandparent, etc.
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
Throws if the file isn't found in any ancestor directory.
|
|
314
|
-
|
|
315
|
-
## Loading raw YAML
|
|
316
|
-
|
|
317
|
-
For cases where you need the raw parsed YAML without schema validation:
|
|
318
|
-
|
|
319
|
-
```typescript
|
|
320
|
-
import { loadRawYaml } from '@db-lyon/flowkit';
|
|
321
|
-
|
|
322
|
-
const data = loadRawYaml('/path/to/file.yml');
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
## Custom schemas
|
|
326
|
-
|
|
327
|
-
`EngineConfigSchema` is the minimal schema flowkit needs. You can extend it for your own config sections:
|
|
328
|
-
|
|
329
|
-
```typescript
|
|
330
|
-
import { z } from 'zod';
|
|
331
|
-
import { EngineConfigSchema } from '@db-lyon/flowkit';
|
|
332
|
-
|
|
333
|
-
const AppConfigSchema = EngineConfigSchema.extend({
|
|
334
|
-
database: z.object({
|
|
335
|
-
host: z.string(),
|
|
336
|
-
port: z.number().default(5432),
|
|
337
|
-
}),
|
|
338
|
-
features: z.record(z.boolean()).default({}),
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
const { config } = loadConfig({
|
|
342
|
-
filename: 'app.yml',
|
|
343
|
-
schema: AppConfigSchema,
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
// config.tasks, config.flows, config.database, config.features
|
|
347
|
-
```
|
|
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
|
+
The inverse task's configured `options` are resolved for `${ns.path}` references as usual, then the `payload` is merged over them. A payload is runtime data the task recorded, not configuration, so it is passed through **literally** — a `${...}` captured inside one reaches the inverse task unchanged.
|
|
193
|
+
|
|
194
|
+
Rollback runs outside any step's scope, so those configured `options` resolve against the host namespaces only. A `${steps.…}` or `${error.…}` in an inverse task's defaults has nothing to resolve against and fails that one rollback record (it is reported in `rollback.errors`; the remaining records still run). Keep inverse-task defaults to host namespaces and put step-derived values in the `payload`.
|
|
195
|
+
|
|
196
|
+
### `agent_prompt` — LLM step
|
|
197
|
+
|
|
198
|
+
When a `LLMProvider` is attached to the context under `ctx.llm`, the built-in `agent_prompt` task invokes it:
|
|
199
|
+
|
|
200
|
+
```yaml
|
|
201
|
+
steps:
|
|
202
|
+
1:
|
|
203
|
+
task: agent_prompt
|
|
204
|
+
options:
|
|
205
|
+
system: "You are a deployment triage agent."
|
|
206
|
+
prompt: "Last error: ${error.message}. Suggest a fix."
|
|
207
|
+
model: claude-opus-4-6
|
|
208
|
+
maxTokens: 512
|
|
209
|
+
schema: { type: object, properties: { fix: { type: string } } } # optional
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Returns `{ text, parsed?, usage? }`. Provider failures become step failures; missing provider is a clear error.
|
|
213
|
+
|
|
214
|
+
## Config layering
|
|
215
|
+
|
|
216
|
+
`loadConfig()` merges up to four layers, left to right:
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
defaults (code) → base file → env overlay → local overlay
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
| Layer | Source | Purpose |
|
|
223
|
+
|-------|--------|---------|
|
|
224
|
+
| 1. Defaults | `options.defaults` in code | Hardcoded fallbacks |
|
|
225
|
+
| 2. Base file | `pipeline.yml` | Project-level config (committed) |
|
|
226
|
+
| 3. Env overlay | `pipeline.staging.yml` | Environment-specific overrides |
|
|
227
|
+
| 4. Local overlay | `pipeline.local.yml` | Developer-specific overrides (gitignored) |
|
|
228
|
+
|
|
229
|
+
### Example
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
import { loadConfig, EngineConfigSchema } from '@db-lyon/flowkit';
|
|
233
|
+
|
|
234
|
+
const { config, configDir } = loadConfig({
|
|
235
|
+
filename: 'pipeline.yml',
|
|
236
|
+
schema: EngineConfigSchema,
|
|
237
|
+
|
|
238
|
+
// Hardcoded defaults merged under everything
|
|
239
|
+
defaults: {
|
|
240
|
+
tasks: {},
|
|
241
|
+
flows: {},
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
// Environment name — loads pipeline.{env}.yml
|
|
245
|
+
env: process.env.NODE_ENV,
|
|
246
|
+
// Or read from a specific env var:
|
|
247
|
+
// envVar: 'APP_ENV',
|
|
248
|
+
|
|
249
|
+
// Directory to search (default: cwd)
|
|
250
|
+
configDir: './config',
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
The `configDir` return value tells you where the config was loaded from.
|
|
255
|
+
|
|
256
|
+
### Environment selection
|
|
257
|
+
|
|
258
|
+
You can specify the environment explicitly or via an env var:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
// Explicit
|
|
262
|
+
loadConfig({ filename: 'app.yml', schema, env: 'production' });
|
|
263
|
+
|
|
264
|
+
// From env var — reads process.env.APP_ENV
|
|
265
|
+
loadConfig({ filename: 'app.yml', schema, envVar: 'APP_ENV' });
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
If both `env` and `envVar` are provided, `env` takes precedence.
|
|
269
|
+
|
|
270
|
+
## Deep merge behavior
|
|
271
|
+
|
|
272
|
+
Config layers are merged using `deepMerge()`, which follows these rules:
|
|
273
|
+
|
|
274
|
+
| Scenario | Behavior |
|
|
275
|
+
|----------|----------|
|
|
276
|
+
| Objects | Recursive key-by-key merge (override wins per-key) |
|
|
277
|
+
| Arrays | Override replaces the base array |
|
|
278
|
+
| Scalars | Override wins |
|
|
279
|
+
| `null` override | Explicitly nullifies the base value |
|
|
280
|
+
| `undefined` override | No-op (base preserved) |
|
|
281
|
+
|
|
282
|
+
### Array append mode
|
|
283
|
+
|
|
284
|
+
By default, arrays in an overlay replace the base array entirely. To append instead, add `__merge: append` to the override array:
|
|
285
|
+
|
|
286
|
+
```yaml
|
|
287
|
+
# base.yml
|
|
288
|
+
plugins:
|
|
289
|
+
- eslint
|
|
290
|
+
- prettier
|
|
291
|
+
|
|
292
|
+
# base.local.yml
|
|
293
|
+
plugins:
|
|
294
|
+
- __merge: append
|
|
295
|
+
- my-custom-plugin
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Result: `['eslint', 'prettier', 'my-custom-plugin']`
|
|
299
|
+
|
|
300
|
+
The `__merge` annotation is stripped from the final array.
|
|
301
|
+
|
|
302
|
+
## Finding config files
|
|
303
|
+
|
|
304
|
+
`findConfigFile()` walks up parent directories to locate a file:
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
import { findConfigFile } from '@db-lyon/flowkit';
|
|
308
|
+
|
|
309
|
+
const path = findConfigFile('pipeline.yml');
|
|
310
|
+
// Searches cwd, then parent, then grandparent, etc.
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Throws if the file isn't found in any ancestor directory.
|
|
314
|
+
|
|
315
|
+
## Loading raw YAML
|
|
316
|
+
|
|
317
|
+
For cases where you need the raw parsed YAML without schema validation:
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
import { loadRawYaml } from '@db-lyon/flowkit';
|
|
321
|
+
|
|
322
|
+
const data = loadRawYaml('/path/to/file.yml');
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## Custom schemas
|
|
326
|
+
|
|
327
|
+
`EngineConfigSchema` is the minimal schema flowkit needs. You can extend it for your own config sections:
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
import { z } from 'zod';
|
|
331
|
+
import { EngineConfigSchema } from '@db-lyon/flowkit';
|
|
332
|
+
|
|
333
|
+
const AppConfigSchema = EngineConfigSchema.extend({
|
|
334
|
+
database: z.object({
|
|
335
|
+
host: z.string(),
|
|
336
|
+
port: z.number().default(5432),
|
|
337
|
+
}),
|
|
338
|
+
features: z.record(z.boolean()).default({}),
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const { config } = loadConfig({
|
|
342
|
+
filename: 'app.yml',
|
|
343
|
+
schema: AppConfigSchema,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// config.tasks, config.flows, config.database, config.features
|
|
347
|
+
```
|