@pgflow/dsl 0.0.5 → 0.0.6

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.
Files changed (71) hide show
  1. package/package.json +4 -1
  2. package/CHANGELOG.md +0 -7
  3. package/__tests__/runtime/flow.test.ts +0 -121
  4. package/__tests__/runtime/steps.test.ts +0 -183
  5. package/__tests__/runtime/utils.test.ts +0 -149
  6. package/__tests__/types/dsl-types.test-d.ts +0 -103
  7. package/__tests__/types/example-flow.test-d.ts +0 -76
  8. package/__tests__/types/extract-flow-input.test-d.ts +0 -71
  9. package/__tests__/types/extract-flow-steps.test-d.ts +0 -74
  10. package/__tests__/types/getStepDefinition.test-d.ts +0 -65
  11. package/__tests__/types/step-input.test-d.ts +0 -212
  12. package/__tests__/types/step-output.test-d.ts +0 -55
  13. package/brainstorming/condition/condition-alternatives.md +0 -219
  14. package/brainstorming/condition/condition-with-flexibility.md +0 -303
  15. package/brainstorming/condition/condition.md +0 -139
  16. package/brainstorming/condition/implementation-plan.md +0 -372
  17. package/brainstorming/dsl/cli-json-schema.md +0 -225
  18. package/brainstorming/dsl/cli.md +0 -179
  19. package/brainstorming/dsl/create-compilator.md +0 -25
  20. package/brainstorming/dsl/dsl-analysis-2.md +0 -166
  21. package/brainstorming/dsl/dsl-analysis.md +0 -512
  22. package/brainstorming/dsl/dsl-critique.md +0 -41
  23. package/brainstorming/fanouts/fanout-subflows-flattened-vs-subruns.md +0 -213
  24. package/brainstorming/fanouts/fanouts-task-index.md +0 -150
  25. package/brainstorming/fanouts/fanouts-with-conditions-and-subflows.md +0 -239
  26. package/brainstorming/subflows/branching.ts.md +0 -38
  27. package/brainstorming/subflows/subflows-callbacks.ts.md +0 -124
  28. package/brainstorming/subflows/subflows-classes.ts.md +0 -83
  29. package/brainstorming/subflows/subflows-flattening-versioned.md +0 -119
  30. package/brainstorming/subflows/subflows-flattening.md +0 -138
  31. package/brainstorming/subflows/subflows.md +0 -118
  32. package/brainstorming/subflows/subruns-table.md +0 -282
  33. package/brainstorming/subflows/subruns.md +0 -315
  34. package/brainstorming/versioning/breaking-and-non-breaking-flow-changes.md +0 -259
  35. package/docs/refactor-edge-worker.md +0 -146
  36. package/docs/versioning.md +0 -19
  37. package/eslint.config.cjs +0 -22
  38. package/out-tsc/vitest/__tests__/runtime/flow.test.d.ts +0 -2
  39. package/out-tsc/vitest/__tests__/runtime/flow.test.d.ts.map +0 -1
  40. package/out-tsc/vitest/__tests__/runtime/steps.test.d.ts +0 -2
  41. package/out-tsc/vitest/__tests__/runtime/steps.test.d.ts.map +0 -1
  42. package/out-tsc/vitest/__tests__/runtime/utils.test.d.ts +0 -2
  43. package/out-tsc/vitest/__tests__/runtime/utils.test.d.ts.map +0 -1
  44. package/out-tsc/vitest/__tests__/types/dsl-types.test-d.d.ts +0 -2
  45. package/out-tsc/vitest/__tests__/types/dsl-types.test-d.d.ts.map +0 -1
  46. package/out-tsc/vitest/__tests__/types/example-flow.test-d.d.ts +0 -2
  47. package/out-tsc/vitest/__tests__/types/example-flow.test-d.d.ts.map +0 -1
  48. package/out-tsc/vitest/__tests__/types/extract-flow-input.test-d.d.ts +0 -2
  49. package/out-tsc/vitest/__tests__/types/extract-flow-input.test-d.d.ts.map +0 -1
  50. package/out-tsc/vitest/__tests__/types/extract-flow-steps.test-d.d.ts +0 -2
  51. package/out-tsc/vitest/__tests__/types/extract-flow-steps.test-d.d.ts.map +0 -1
  52. package/out-tsc/vitest/__tests__/types/getStepDefinition.test-d.d.ts +0 -2
  53. package/out-tsc/vitest/__tests__/types/getStepDefinition.test-d.d.ts.map +0 -1
  54. package/out-tsc/vitest/__tests__/types/step-input.test-d.d.ts +0 -2
  55. package/out-tsc/vitest/__tests__/types/step-input.test-d.d.ts.map +0 -1
  56. package/out-tsc/vitest/__tests__/types/step-output.test-d.d.ts +0 -2
  57. package/out-tsc/vitest/__tests__/types/step-output.test-d.d.ts.map +0 -1
  58. package/out-tsc/vitest/tsconfig.spec.tsbuildinfo +0 -1
  59. package/out-tsc/vitest/vite.config.d.ts +0 -3
  60. package/out-tsc/vitest/vite.config.d.ts.map +0 -1
  61. package/project.json +0 -28
  62. package/prompts/edge-worker-refactor.md +0 -105
  63. package/src/dsl.ts +0 -318
  64. package/src/example-flow.ts +0 -67
  65. package/src/index.ts +0 -1
  66. package/src/utils.ts +0 -84
  67. package/tsconfig.json +0 -13
  68. package/tsconfig.lib.json +0 -26
  69. package/tsconfig.spec.json +0 -35
  70. package/typecheck.log +0 -120
  71. package/vite.config.ts +0 -57
@@ -1,282 +0,0 @@
1
- # **Next Steps: Implementing Subflows (Subruns) with Parent References and Indexing**
2
-
3
- This document outlines a plan for implementing subflows—referred to here as “subruns”—in a manner that supports branching, reusable workflow components, and clear database visibility. We will explore two distinct approaches for storing subruns, clarify parent–child relationships, and introduce an indexing strategy (`run_index` / `subrun_index`) for keeping the data well-structured and queryable.
4
-
5
- ---
6
- ## **1. Overview**
7
-
8
- Subflows allow you to embed smaller flows within a larger “parent” flow, keeping logic modular and reusable. To make subflows first-class entities:
9
-
10
- - **We treat each subflow instance as a “subrun,”** which references:
11
- 1. A `parent_run_id` (the main workflow run).
12
- 2. A `parent_step_slug` (the step in the parent workflow that triggers the subflow).
13
- - **We store indexing details** (`run_index`, `subrun_index`, etc.) to easily query or filter subruns within the database or logs.
14
-
15
- The key questions revolve around how to store subruns:
16
-
17
- 1. **Approach A**: Subruns live in the same `runs` table.
18
- 2. **Approach B**: Subruns get their own dedicated `subruns` table.
19
-
20
- Both approaches support referencing the parent’s run and step, but differ in how data is physically separated or combined.
21
-
22
- ---
23
- ## **2. Key Requirements**
24
-
25
- 1. **Parent–Child Links**
26
- Each subrun must store:
27
- - `parent_run_id`: The primary key from the parent run.
28
- - `parent_step_slug`: The step in the parent flow that spawns this subflow.
29
-
30
- 2. **Unique Identification**
31
- We need a clear way to identify each subrun. This might involve:
32
- - Generating a unique `subrun_id` separate from the parent run.
33
- - Combining `run_id` with a sub-run index (e.g., `run_id = 123` and `subrun_index = 1`).
34
-
35
- 3. **Indexing / Retrieval**
36
- - We want to quickly query all subruns of a particular parent run, or filter by `parent_step_slug`.
37
- - The indexing strategy (`run_index`, `subrun_index`) should be consistent for easy debugging and UI representation.
38
-
39
- 4. **Branching and Type-Safe Mapping**
40
- - The subflow DSL must let the parent flow pass input fields to the subflow and retrieve the output fields.
41
- - Dependencies in the parent flow can depend on the subflow’s “final steps” or treat the subflow as a single virtual step.
42
-
43
- 5. **Compatibility with Skipping / runIf / runUnless** *(Optional but recommended)*
44
- - If the parent step is skipped, the subflow is also effectively skipped.
45
- - The engine logic can uniformly handle “runIf,” “runUnless,” or other conditional logic for subflows.
46
-
47
- ---
48
- ## **3. Two Approaches to Storing Subruns**
49
-
50
- ### **Approach A: Store Subruns in the `runs` Table**
51
-
52
- **Core Idea**
53
- We continue to use the existing `runs` table for everything (both parent runs and child subruns). Each subrun row has columns:
54
-
55
- - `id` (PK for the subrun)
56
- - `parent_run_id` (nullable—if `NULL`, it’s a top-level run)
57
- - `parent_step_slug` (nullable—if `NULL`, it’s a top-level run)
58
- - `flow_slug` (referencing which flow definition this run is for)
59
- - *Optional:* `run_index` if you want to separate the notion of subruns from runs
60
-
61
- **Pros**
62
- 1. Simpler to keep a single table: easy to reuse existing queries for “all runs.”
63
- 2. We can make queries that unify top-level flows and subflows in one pass.
64
- 3. No major rearchitecture beyond adding a few columns for `parent_run_id` and `parent_step_slug`.
65
-
66
- **Cons**
67
- 1. The `runs` table can become large and cluttered with both top-level and subflow entries.
68
- 2. Might require a “subrun vs. run” type flag to differentiate them when listing.
69
- 3. If you want to drastically different schema for subruns, you’ll have to embed specialized columns in the same table.
70
-
71
- **Example Schema Changes**
72
-
73
- ```sql
74
- ALTER TABLE pgflow.runs
75
- ADD COLUMN parent_run_id INT NULL,
76
- ADD COLUMN parent_step_slug TEXT NULL,
77
- ADD COLUMN run_index INT NULL; -- e.g. "this is subrun #2 of run #100"
78
- ```
79
-
80
- **Runtime Mechanics**
81
- - When the parent step triggers the subflow, the engine inserts a new row into `runs`.
82
- - `parent_run_id` references the parent run’s ID.
83
- - `parent_step_slug` identifies the step that spawned the subflow.
84
- - The engine schedules steps within this subrun as usual, but can track the child run’s status to determine when the subflow is “done.”
85
-
86
- ### **Approach B: Store Subruns in a Dedicated `subruns` Table**
87
-
88
- **Core Idea**
89
- Create a new table, say `pgflow.subruns`, that specifically holds the subflow runs. Each record might have:
90
-
91
- - `subrun_id` (PK in this dedicated table)
92
- - `parent_run_id` (FK to `runs.id`)
93
- - `parent_step_slug`
94
- - `subflow_slug` (which flow definition the subrun is for)
95
- - `subrun_index`
96
-
97
- **Pros**
98
- 1. Clear separation of top-level runs (`pgflow.runs`) from subruns (`pgflow.subruns`).
99
- 2. Potentially simpler queries if you only want subruns or only want top-level runs.
100
- 3. Allows you to store subflow-specific metadata without cluttering the main runs table.
101
-
102
- **Cons**
103
- 1. You need to join between `runs` and `subruns` to get a complete picture of all running flows.
104
- 2. Additional code is required to manage subrun creation vs. run creation.
105
- 3. Some queries or logs might be split across two tables, which can add complexity.
106
-
107
- **Example Schema**
108
-
109
- ```sql
110
- CREATE TABLE pgflow.subruns (
111
- subrun_id SERIAL PRIMARY KEY,
112
- parent_run_id INT NOT NULL REFERENCES pgflow.runs(id),
113
- parent_step_slug TEXT NOT NULL,
114
- subflow_slug TEXT NOT NULL,
115
- subrun_index INT NOT NULL,
116
- status TEXT NOT NULL DEFAULT 'created',
117
- created_at TIMESTAMP DEFAULT now(),
118
- updated_at TIMESTAMP DEFAULT now()
119
- );
120
- ```
121
-
122
- Where “step” or “log” data for the subrun can link back via `subrun_id` instead of `run_id`.
123
-
124
- **Runtime Mechanics**
125
- - The parent flow step triggers creation of a `subruns` record.
126
- - Steps specifically for that subflow insert states/outputs referencing `subrun_id` instead of `run_id` (or referencing `run_id` plus `subrun_id`, depending on your design).
127
- - The subflow DSL completes, the subrun is marked “completed” or “failed.” The parent can then proceed.
128
-
129
- ---
130
- ## **4. Tracking `run_index` / `subrun_index`**
131
-
132
- Regardless of the approach, having an index helps group or order subruns:
133
-
134
- - **`run_index`**: an integer auto-increment within a single parent run to identify subflow invocations. For example, the first subrun triggered in a run might have `run_index = 1`, the second subrun `run_index = 2`, etc.
135
- - **`subrun_index`**: in Approach B, if the subflow can itself contain nested subflows, you might keep an additional nested index. Example: subflow’s child subflows each have their own index.
136
-
137
- This structure helps to:
138
-
139
- 1. Easily see “which subrun is which” if the same subflow is triggered multiple times within the same parent run.
140
- 2. Show a user-friendly subflow numbering in logs and UI.
141
-
142
- Here’s a possible approach:
143
-
144
- 1. **On subrun creation**, query the max `run_index` from existing subruns with the same `parent_run_id` and increment by 1.
145
- 2. Store that in the `run_index` column of the subrun row.
146
-
147
- If you want nested subflows beyond one level, you could store a dotted path like “1.3.2” or a separate “parent_subrun_id,” but that’s a more advanced scenario.
148
-
149
- ---
150
- ## **5. Database Schema Adjustments**
151
-
152
- ### **If We Go with a Single `runs` Table (Approach A)**
153
-
154
- 1. **Add columns** to `pgflow.runs`:
155
-
156
- ```sql
157
- ALTER TABLE pgflow.runs
158
- ADD COLUMN parent_run_id INT NULL,
159
- ADD COLUMN parent_step_slug TEXT NULL,
160
- ADD COLUMN run_index INT NULL;
161
- ```
162
-
163
- 2. **Add an index** if we want quick lookups:
164
-
165
- ```sql
166
- CREATE INDEX idx_runs_parent
167
- ON pgflow.runs (parent_run_id, parent_step_slug);
168
- ```
169
-
170
- 3. **Optional**: add a `run_type` column with `'top-level'` or `'subrun'` to easily differentiate.
171
-
172
- ### **If We Use a `subruns` Table (Approach B)**
173
-
174
- 1. **Create new table**:
175
-
176
- ```sql
177
- CREATE TABLE pgflow.subruns (
178
- subrun_id SERIAL PRIMARY KEY,
179
- parent_run_id INT NOT NULL REFERENCES pgflow.runs(id),
180
- parent_step_slug TEXT NOT NULL,
181
- subflow_slug TEXT NOT NULL,
182
- subrun_index INT NOT NULL,
183
- status TEXT NOT NULL DEFAULT 'created',
184
- created_at TIMESTAMP DEFAULT now(),
185
- updated_at TIMESTAMP DEFAULT now()
186
- );
187
- ```
188
-
189
- 2. **Changing Step Storage**:
190
- - Steps for a subrun either go into `pgflow.steps` referencing “`subrun_id`” or, if you keep them in the same table as the parent, they must store a new field like `subrun_id` or “`(run_id, subrun_id)`.”
191
-
192
- 3. **Parent–Child Consistency**
193
- - If the parent run is canceled or fails, do we also cascade that to subruns? You might create triggers or handle it in your application logic.
194
-
195
- ---
196
- ## **6. DSL & Engine Changes**
197
-
198
- 1. **DSL**
199
- - Add `.subflow({ slug: 'mySubflow', flow: PaymentFlow, … })` syntax.
200
- - Under the hood, the system decides how to store subflow invocation:
201
- - **Approach A**: Insert a new row in the `runs` table with `parent_run_id` referencing the parent.
202
- - **Approach B**: Insert a row in `subruns`, referencing the parent run in `parent_run_id`.
203
-
204
- 2. **Referencing Inputs & Outputs**
205
- - The parent flow maps certain outputs to the subflow’s inputs (type-safe).
206
- - The subflow’s final steps produce outputs, which are attached to the subrun.
207
- - Parent steps can depend on “subflow completed.” The DSL can treat subflow as a single step or expand dependencies in detail.
208
-
209
- 3. **Engine Execution**
210
- - When the parent step triggers the subflow, the engine marks the parent step as “completed” or “in progress” and spins up the subrun.
211
- - The subrun runs its steps, referencing `parent_run_id` and `parent_step_slug` (or `subrun_id`).
212
- - On subrun completion, the parent flow sees subflow output and can proceed.
213
-
214
- 4. **Conditional Skipping** *(integrated with subruns)*
215
- - If your DSL or engine supports `runIf` / `runUnless`, a skipped subflow “root step” immediately marks the entire subrun as `skipped`.
216
- - For partial skipping inside a subflow, the same logic applies to subrun steps.
217
-
218
- ---
219
- ## **7. Actionable Steps**
220
-
221
- Below is a consolidated checklist to guide implementation.
222
-
223
- 1. **Decide on Storage Approach (A or B)**
224
- - If you prefer a simpler single-table approach, add `parent_run_id`, `parent_step_slug`, `run_index` to `runs`.
225
- - If you want stronger separation, implement a new `subruns` table.
226
-
227
- 2. **Add Index/Reference Columns**
228
- - Set up `run_index` or `subrun_index` for subflow identification.
229
- - For Approach A, you might do something like:
230
-
231
- ```sql
232
- ALTER TABLE pgflow.runs
233
- ADD COLUMN parent_run_id INT NULL,
234
- ADD COLUMN parent_step_slug TEXT NULL,
235
- ADD COLUMN run_index INT NULL;
236
- ```
237
-
238
- 3. **Extend the DSL to Create Subruns**
239
- - On `.subflow()`, create a new row in `runs` or `subruns`.
240
- - Store the parent references: `parent_run_id`, `parent_step_slug`, and computed `run_index`.
241
-
242
- 4. **Implement Subflow Execution Logic in the Engine**
243
- - Engine receives a new subrun record → sets up tasks for the subflow’s steps.
244
- - On completion, store subflow outputs in a determined location (could be `runs.output` or a dedicated subrun output table).
245
-
246
- 5. **Ensure Parent–Child Status Aggregation**
247
- - If subflow finishes successfully, the parent step referencing it moves to “completed.”
248
- - If subflow fails or is canceled, the parent step might fail or skip.
249
- - Optional: cascade parent run cancellation → subruns canceled.
250
-
251
- 6. **(Optional) Integrate Conditionals/Skipping**
252
- - If skipping logic is relevant, ensure subflow “root steps” can be marked skipped.
253
- - Entire subrun = skipped if the conditions or parent step are skipped.
254
-
255
- 7. **UI / Logging Updates**
256
- - In logs or a UI console, display subruns under the parent run + parent step.
257
- - If using `run_index`, label them systematically, e.g. “Subrun #2 for parent step ‘process_payment.’”
258
-
259
- 8. **Testing & Validation**
260
- - Test subflow creation, ensuring parent–child references are correct.
261
- - Test concurrency or partial failures.
262
- - Verify that outputs from subruns can be correctly accessed and used by downstream parent steps.
263
-
264
- ---
265
-
266
- ## **Conclusion**
267
-
268
- By implementing subruns with a clear link to the parent run and parent step, you enable:
269
-
270
- 1. **Reusable “Functional” Subflows**
271
- Each subflow can have well-defined inputs/outputs, making your main flows simpler.
272
-
273
- 2. **Robust DB Visibility**
274
- Whether you store subruns in the main `runs` table or a dedicated `subruns` table, you gain the ability to debug and query subflows natively.
275
-
276
- 3. **Configurable Branching & Indexing**
277
- Using `run_index` / `subrun_index` helps track multiple subflows triggered by the same parent flow or step.
278
-
279
- 4. **Extensibility**
280
- The same approach can be extended for nested subflows or advanced features like skipping logic, partial concurrency limits, or subflow-specific resource constraints.
281
-
282
- By following the steps above—especially deciding on a storage approach and implementing consistent parent–child references plus indexing—you can maintain a clean, scalable workflow engine that offers both power and clarity for subflow operations.
@@ -1,315 +0,0 @@
1
- # Next Steps: Implementing Conditional Skips and Subflow Logic
2
-
3
- Below is an updated, comprehensive plan that merges two key initiatives:
4
-
5
- 1. **Conditional Step Execution**: Introducing `runIf` / `runUnless` and a `skipped` status.
6
- 2. **Subflows / Branching**: Implementing subflows without a separate “subruns” table, referencing the parent run and parent step, and introducing a `run_index` (or `subrun_index`) for clarity.
7
-
8
- This document walks through the new database schema changes, DSL enhancements, engine logic, and the final actionable steps you can follow.
9
-
10
- ---
11
-
12
- ## Table of Contents
13
-
14
- 1. [Stage 1: DB Schema Enhancements](#stage-1-db-schema-enhancements)
15
- 1.1. [Condition Columns for `runIf` / `runUnless`](#1-condition-columns-for-runif--rununless)
16
- 1.2. [New `skipped` Status](#2-new-skipped-status)
17
- 1.3. [Subflow Columns: `parent_run_id`, `parent_step_slug`, `run_index`](#3-subflow-columns-parent_run_id-parent_step_slug-run_index)
18
-
19
- 2. [Stage 2: Extend the DSL](#stage-2-extend-the-dsl)
20
- 2.1. [Defining `runIf` / `runUnless` in `.step()`](#21-defining-runif--rununless-in-step)
21
- 2.2. [Defining Subflows via `.subflow()`](#22-defining-subflows-via-subflow)
22
-
23
- 3. [Stage 3: Implementing the Engine Logic](#stage-3-implementing-the-engine-logic)
24
- 3.1. [Evaluating Conditions and Skipping Steps](#31-evaluating-conditions-and-skipping-steps)
25
- 3.2. [Attaching Subflow Execution to Parent Steps](#32-attaching-subflow-execution-to-parent-steps)
26
-
27
- 4. [Stage 4: Storing Outputs in `step_states` (Optional)](#stage-4-storing-outputs-in-step_states-optional)
28
-
29
- 5. [Actionable Steps (Checklist)](#actionable-steps-checklist)
30
-
31
- ---
32
-
33
- ## Stage 1: DB Schema Enhancements
34
-
35
- ### 1) Condition Columns for `runIf` / `runUnless`
36
-
37
- **Current State**
38
- We do not store any explicit conditional logic columns. The skipping concept is absent from the DB schema.
39
-
40
- **Change**
41
- Add two new columns to the `steps` table (or your equivalent) to store JSON-based conditions:
42
-
43
- ```sql
44
- ALTER TABLE pgflow.steps
45
- ADD COLUMN run_if_condition JSONB DEFAULT NULL;
46
-
47
- ALTER TABLE pgflow.steps
48
- ADD COLUMN run_unless_condition JSONB DEFAULT NULL;
49
- ```
50
-
51
- - `run_if_condition`: If present, the engine will only run the step if the condition is satisfied.
52
- - `run_unless_condition`: If present, the engine will only run the step if this condition is *not* satisfied.
53
-
54
- **Why JSONB?**
55
- Conditions can be complex. Storing them in JSONB offers flexibility. A single step might rely on multiple inputs, or you may want to nest logic in your condition structure.
56
-
57
- ---
58
-
59
- ### 2) New `skipped` Status
60
-
61
- **Current State**
62
- Our `step_states.status` typically contains:
63
- - `created`
64
- - `started`
65
- - `completed`
66
- - `failed`
67
-
68
- **Change**
69
- Add a new status `skipped`, then update constraints accordingly:
70
-
71
- ```sql
72
- ALTER TABLE pgflow.step_states
73
- DROP CONSTRAINT step_states_status_check,
74
- ADD CONSTRAINT step_states_status_check CHECK (
75
- status IN ('created', 'started', 'completed', 'failed', 'skipped')
76
- );
77
- ```
78
-
79
- This status is used when a step’s conditions are not met. As soon as we decide a step (and any of its transitive dependents) cannot run, we mark it as `skipped`.
80
-
81
- ---
82
-
83
- ### 3) Subflow Columns: `parent_run_id`, `parent_step_slug`, `run_index`
84
-
85
- **Current State**
86
- We sometimes flatten subflows into the main `steps` table. Alternatively, we could create a separate `subruns` table. We want to avoid that extra overhead.
87
-
88
- **Change**
89
- **We do NOT create a separate `subruns` table.** Instead, we enhance `steps` to reference the parent run and step, plus track a `run_index` (or `subrun_index`):
90
-
91
- ```sql
92
- ALTER TABLE pgflow.steps
93
- ADD COLUMN parent_run_id INT NULL,
94
- ADD COLUMN parent_step_slug TEXT NULL,
95
- ADD COLUMN run_index INT NULL; -- or "subrun_index"
96
- ```
97
-
98
- - **`parent_run_id`**: Points back to the run that spawned this subflow. If it’s a top-level flow, this can be `NULL`.
99
- - **`parent_step_slug`**: Identifies which parent step triggered the subflow.
100
- - **`run_index` or `subrun_index`**: A numeric counter or sequence to differentiate multiple subflows invoked by the same parent step.
101
-
102
- **Usage**
103
- - When a parent flow step spawns a subflow, we create new step records for that subflow, all tied to the same top-level `run_id` as the parent (if desired) or a new run row but referencing `parent_run_id` in either case.
104
- - Subflow steps can be grouped by `(run_id, parent_step_slug, run_index)` or `(parent_run_id, ...)` in queries.
105
- - The subflow steps remain in the same table for easy flattening in logs/UI but still retain “who spawned me?” metadata.
106
-
107
- ---
108
-
109
- ## Stage 2: Extend the DSL
110
-
111
- ### 2.1 Defining `runIf` / `runUnless` in `.step()`
112
-
113
- **Current State**
114
- The DSL for `.step()` usually accepts something like:
115
-
116
- ```ts
117
- flow.step(
118
- {
119
- slug: 'myStep',
120
- dependsOn: ['someOtherStep'],
121
- // ...
122
- },
123
- async (inputs) => { /* step logic */ }
124
- );
125
- ```
126
-
127
- **Change**
128
- Allow users to provide `runIf` / `runUnless`:
129
-
130
- ```ts
131
- flow.step(
132
- {
133
- slug: "myStep",
134
- dependsOn: ["someOtherStep"],
135
- runIf: { run: { shouldSendEmail: true } },
136
- runUnless: { run: { userIsDeactivated: true } },
137
- },
138
- async (input) => {
139
- // ...
140
- }
141
- );
142
- ```
143
-
144
- **Implementation Details**
145
- When the flow is compiled (or “registered”), store these condition objects in the new `run_if_condition` / `run_unless_condition` columns. They will be evaluated by the engine later (see [Stage 3](#stage-3-implementing-the-engine-logic)).
146
-
147
- ---
148
-
149
- ### 2.2 Defining Subflows via `.subflow()`
150
-
151
- **Current State**
152
- We either flatten subflows or run them as fully separate flows. We want to unify them with minimal overhead.
153
-
154
- **Change**
155
- Introduce or refine a `.subflow()` DSL method that looks roughly like:
156
-
157
- ```ts
158
- flow.subflow({
159
- slug: 'mySubflowStep',
160
- dependsOn: ['previousStep'],
161
- runIndex: 1, // optional or auto-generated
162
- flow: PaymentFlow, // an existing Flow object
163
- input: (parentOutputs) => ({
164
- // map parent outputs to subflow inputs
165
- }),
166
- output: (subflowOutputs) => ({
167
- // transform the subflow's final outputs for the parent
168
- }),
169
- });
170
- ```
171
-
172
- **Storing in the DB**
173
- - When `.subflow()` is used, we create entries in `steps` for each subflow step.
174
- - We also set:
175
- - `parent_run_id = <the run_id of the parent>`
176
- - `parent_step_slug = 'mySubflowStep'` (or the subflow’s "root" slug)
177
- - `run_index = runIndex || (some auto counter)`
178
-
179
- **Outcome**
180
- A single “parent step” record appears in the main flow’s step list, plus multiple child steps that reference this “parent step.” They remain in the same `steps` table, but we can query them by `parent_step_slug`.
181
-
182
- ---
183
-
184
- ## Stage 3: Implementing the Engine Logic
185
-
186
- ### 3.1 Evaluating Conditions and Skipping Steps
187
-
188
- When a step transitions from `created` → `started`, the engine must:
189
-
190
- 1. **Gather Inputs**: Merge all relevant run data, dependency outputs, etc.
191
- 2. **Evaluate `runIf`**: If present, must be satisfied for the step to run.
192
- 3. **Evaluate `runUnless`**: If present, must *not* be satisfied for the step to run.
193
- 4. **Decide**:
194
- - If both checks pass, mark the step as `started` (and enqueue tasks).
195
- - If they fail, mark the step as `skipped` and do a recursive skip of its dependents.
196
-
197
- **Pseudo-SQL** (conceptual):
198
-
199
- ```sql
200
- WITH step_def AS (
201
- SELECT s.run_if_condition, s.run_unless_condition
202
- FROM pgflow.steps s
203
- WHERE s.run_id = ...
204
- AND s.step_slug = ...
205
- ),
206
- input_data AS (
207
- SELECT ... merged_input ...
208
- )
209
- SELECT CASE
210
- WHEN step_def.run_if_condition IS NOT NULL
211
- AND NOT evaluate_json_condition(input_data, step_def.run_if_condition)
212
- THEN true -- skip
213
- WHEN step_def.run_unless_condition IS NOT NULL
214
- AND evaluate_json_condition(input_data, step_def.run_unless_condition)
215
- THEN true -- skip
216
- ELSE false
217
- END as should_skip;
218
- ```
219
-
220
- If `should_skip = true`, we set `step_states.status = 'skipped'` and skip all dependent steps.
221
-
222
- ---
223
-
224
- ### 3.2 Attaching Subflow Execution to Parent Steps
225
-
226
- When a parent flow step triggers a subflow:
227
-
228
- 1. **Subflow Steps**: Created in the DB, referencing `parent_run_id`, `parent_step_slug`, and `run_index`.
229
- 2. **Dependency**: The subflow’s “root” step typically depends on the “parent step.” The engine must ensure the parent step is `completed` (or not skipped) before scheduling the subflow steps.
230
- 3. **Execution**: Each subflow step runs or is skipped the same way any normal step does—no separate table needed.
231
- 4. **Completion**:
232
- - Once the subflow’s “final step(s)” complete, the DSL merges outputs back into the parent flow’s step named `mySubflowStep`.
233
- - The parent flow can then continue to a next step that depends on `mySubflowStep`.
234
-
235
- **Cascading Skips**
236
- If the subflow’s parent step is skipped, all subflow steps mark themselves as skipped automatically (`parent_step_slug` can be used to find them).
237
-
238
- ---
239
-
240
- ## Stage 4: Storing Outputs in `step_states` (Optional)
241
-
242
- Currently we store step outputs in `step_tasks.output`. If you want quick access to each step’s final results (without querying tasks):
243
-
244
- ```sql
245
- ALTER TABLE pgflow.step_states
246
- ADD COLUMN output JSONB DEFAULT NULL;
247
- ```
248
-
249
- - **On Task Completion**: Copy the final result into `step_states.output`.
250
- - **Trade-Off**: This duplicates data but simplifies queries and usage of subflow outputs.
251
-
252
- ---
253
-
254
- ## Actionable Steps (Checklist)
255
-
256
- Below is a summarized TODO list:
257
-
258
- 1. **Add Condition Columns**
259
- - `[Stage 1.1]`
260
- ```sql
261
- ALTER TABLE pgflow.steps
262
- ADD COLUMN run_if_condition JSONB DEFAULT NULL,
263
- ADD COLUMN run_unless_condition JSONB DEFAULT NULL;
264
- ```
265
-
266
- 2. **Allow "skipped" in `step_states`**
267
- - `[Stage 1.2]`
268
- ```sql
269
- ALTER TABLE pgflow.step_states
270
- DROP CONSTRAINT step_states_status_check,
271
- ADD CONSTRAINT step_states_status_check CHECK (
272
- status IN ('created', 'started', 'completed', 'failed', 'skipped')
273
- );
274
- ```
275
-
276
- 3. **Add Subflow Columns**
277
- - `[Stage 1.3]`
278
- ```sql
279
- ALTER TABLE pgflow.steps
280
- ADD COLUMN parent_run_id INT NULL,
281
- ADD COLUMN parent_step_slug TEXT NULL,
282
- ADD COLUMN run_index INT NULL;
283
- ```
284
-
285
- 4. **Enhance DSL for `runIf` / `runUnless`**
286
- - `[Stage 2.1]`
287
- - Let `.step()` accept `runIf` and `runUnless`.
288
- - During compilation, store them in the new columns.
289
-
290
- 5. **Add `.subflow()` DSL Method**
291
- - `[Stage 2.2]`
292
- - Generate subflow steps (still in the same `steps` table) referencing `parent_run_id`, `parent_step_slug`, and `run_index`.
293
-
294
- 6. **Implement Engine Check**
295
- - `[Stage 3.1]`
296
- - Before moving a step to `started`, evaluate `runIf` / `runUnless`.
297
- - Mark step (and transitive dependents) as `skipped` if conditions fail.
298
-
299
- 7. **Cascade Skips for Subflows**
300
- - `[Stage 3.2]`
301
- - If the parent step is skipped, ensure all child subflow steps become `skipped`.
302
-
303
- 8. **(Optional) Store Outputs in `step_states`**
304
- - `[Stage 4]`
305
- - Add `output` column in `step_states`.
306
- - Update it when tasks complete.
307
-
308
- By following these steps, you will have:
309
-
310
- - **Robust Conditional Execution** showing clearly in both the DSL and DB.
311
- - **Subflows without a separate `subruns` table**, preserving a simple, flattened approach while still tracking parent-child relationships.
312
- - **Full or partial** type-safe references to subflow inputs/outputs in the parent.
313
- - **Simplified Queries** for final outputs (if you add the optional `step_states.output` column).
314
-
315
- This integrated plan keeps the codebase more maintainable, offers better debugging, and aligns smoothly with advanced use cases (like branching, reusable subflows, or deeper concurrency management) in a single, unified workflow engine.