@pgflow/dsl 0.0.5-prealpha.2 → 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 (70) hide show
  1. package/package.json +4 -1
  2. package/__tests__/runtime/flow.test.ts +0 -121
  3. package/__tests__/runtime/steps.test.ts +0 -183
  4. package/__tests__/runtime/utils.test.ts +0 -149
  5. package/__tests__/types/dsl-types.test-d.ts +0 -103
  6. package/__tests__/types/example-flow.test-d.ts +0 -76
  7. package/__tests__/types/extract-flow-input.test-d.ts +0 -71
  8. package/__tests__/types/extract-flow-steps.test-d.ts +0 -74
  9. package/__tests__/types/getStepDefinition.test-d.ts +0 -65
  10. package/__tests__/types/step-input.test-d.ts +0 -212
  11. package/__tests__/types/step-output.test-d.ts +0 -55
  12. package/brainstorming/condition/condition-alternatives.md +0 -219
  13. package/brainstorming/condition/condition-with-flexibility.md +0 -303
  14. package/brainstorming/condition/condition.md +0 -139
  15. package/brainstorming/condition/implementation-plan.md +0 -372
  16. package/brainstorming/dsl/cli-json-schema.md +0 -225
  17. package/brainstorming/dsl/cli.md +0 -179
  18. package/brainstorming/dsl/create-compilator.md +0 -25
  19. package/brainstorming/dsl/dsl-analysis-2.md +0 -166
  20. package/brainstorming/dsl/dsl-analysis.md +0 -512
  21. package/brainstorming/dsl/dsl-critique.md +0 -41
  22. package/brainstorming/fanouts/fanout-subflows-flattened-vs-subruns.md +0 -213
  23. package/brainstorming/fanouts/fanouts-task-index.md +0 -150
  24. package/brainstorming/fanouts/fanouts-with-conditions-and-subflows.md +0 -239
  25. package/brainstorming/subflows/branching.ts.md +0 -38
  26. package/brainstorming/subflows/subflows-callbacks.ts.md +0 -124
  27. package/brainstorming/subflows/subflows-classes.ts.md +0 -83
  28. package/brainstorming/subflows/subflows-flattening-versioned.md +0 -119
  29. package/brainstorming/subflows/subflows-flattening.md +0 -138
  30. package/brainstorming/subflows/subflows.md +0 -118
  31. package/brainstorming/subflows/subruns-table.md +0 -282
  32. package/brainstorming/subflows/subruns.md +0 -315
  33. package/brainstorming/versioning/breaking-and-non-breaking-flow-changes.md +0 -259
  34. package/docs/refactor-edge-worker.md +0 -146
  35. package/docs/versioning.md +0 -19
  36. package/eslint.config.cjs +0 -22
  37. package/out-tsc/vitest/__tests__/runtime/flow.test.d.ts +0 -2
  38. package/out-tsc/vitest/__tests__/runtime/flow.test.d.ts.map +0 -1
  39. package/out-tsc/vitest/__tests__/runtime/steps.test.d.ts +0 -2
  40. package/out-tsc/vitest/__tests__/runtime/steps.test.d.ts.map +0 -1
  41. package/out-tsc/vitest/__tests__/runtime/utils.test.d.ts +0 -2
  42. package/out-tsc/vitest/__tests__/runtime/utils.test.d.ts.map +0 -1
  43. package/out-tsc/vitest/__tests__/types/dsl-types.test-d.d.ts +0 -2
  44. package/out-tsc/vitest/__tests__/types/dsl-types.test-d.d.ts.map +0 -1
  45. package/out-tsc/vitest/__tests__/types/example-flow.test-d.d.ts +0 -2
  46. package/out-tsc/vitest/__tests__/types/example-flow.test-d.d.ts.map +0 -1
  47. package/out-tsc/vitest/__tests__/types/extract-flow-input.test-d.d.ts +0 -2
  48. package/out-tsc/vitest/__tests__/types/extract-flow-input.test-d.d.ts.map +0 -1
  49. package/out-tsc/vitest/__tests__/types/extract-flow-steps.test-d.d.ts +0 -2
  50. package/out-tsc/vitest/__tests__/types/extract-flow-steps.test-d.d.ts.map +0 -1
  51. package/out-tsc/vitest/__tests__/types/getStepDefinition.test-d.d.ts +0 -2
  52. package/out-tsc/vitest/__tests__/types/getStepDefinition.test-d.d.ts.map +0 -1
  53. package/out-tsc/vitest/__tests__/types/step-input.test-d.d.ts +0 -2
  54. package/out-tsc/vitest/__tests__/types/step-input.test-d.d.ts.map +0 -1
  55. package/out-tsc/vitest/__tests__/types/step-output.test-d.d.ts +0 -2
  56. package/out-tsc/vitest/__tests__/types/step-output.test-d.d.ts.map +0 -1
  57. package/out-tsc/vitest/tsconfig.spec.tsbuildinfo +0 -1
  58. package/out-tsc/vitest/vite.config.d.ts +0 -3
  59. package/out-tsc/vitest/vite.config.d.ts.map +0 -1
  60. package/project.json +0 -28
  61. package/prompts/edge-worker-refactor.md +0 -105
  62. package/src/dsl.ts +0 -318
  63. package/src/example-flow.ts +0 -67
  64. package/src/index.ts +0 -1
  65. package/src/utils.ts +0 -84
  66. package/tsconfig.json +0 -13
  67. package/tsconfig.lib.json +0 -26
  68. package/tsconfig.spec.json +0 -35
  69. package/typecheck.log +0 -120
  70. package/vite.config.ts +0 -57
@@ -1,124 +0,0 @@
1
- import { Flow } from './dsl.ts';
2
-
3
- // Define the input type for our flow
4
- type Input = {
5
- query: string;
6
- preferredLanguage?: string;
7
- };
8
-
9
- const AnswerTranslatedFlow = (subflow) =>
10
- subflow
11
- .step(
12
- { slug: 'translate' },
13
- async (input) =>
14
- await translateText(
15
- input.run.query,
16
- input.run.preferredLanguage || 'english'
17
- )
18
- )
19
- .step(
20
- { slug: 'answerTranslated' },
21
- async (input) =>
22
- await generateAnswer(
23
- input.translate.translatedText,
24
- input.run.preferredLanguage || 'english'
25
- )
26
- );
27
-
28
- const AnswerTranslatedFlow = new Flow<string>({})
29
- .step(
30
- { slug: 'translate' },
31
- async (input) =>
32
- await translateText(
33
- input.run.query,
34
- input.run.preferredLanguage || 'english'
35
- )
36
- )
37
- .step(
38
- { slug: 'answerTranslated' },
39
- async (input) =>
40
- await generateAnswer(
41
- input.translate.translatedText,
42
- input.run.preferredLanguage || 'english'
43
- )
44
- );
45
-
46
- // Using the new subflow DSL method
47
- export const QueryAnswerFlow = new Flow<Input>({
48
- slug: 'query_answer',
49
- maxAttempts: 3,
50
- baseDelay: 5,
51
- timeout: 60,
52
- })
53
- .step(
54
- { slug: 'detectLanguage' },
55
- async (input) => await detectLanguage(input.run.query)
56
- )
57
- .subflow(
58
- {
59
- slug: 'translation',
60
- runIf: {
61
- detectLanguage: (result) =>
62
- result.language !== input.run.preferredLanguage &&
63
- input.run.preferredLanguage,
64
- },
65
- },
66
- AnswerTranslatedFlow
67
- )
68
- .subflow(
69
- {
70
- slug: 'directAnswer',
71
- runIf: {
72
- detectLanguage: (result) =>
73
- result.language === input.run.preferredLanguage ||
74
- !input.run.preferredLanguage,
75
- },
76
- },
77
- (subflow) =>
78
- subflow.step(
79
- { slug: 'answerDirect' },
80
- async (input) =>
81
- await generateAnswer(input.run.query, input.detectLanguage.language)
82
- )
83
- )
84
- .subflow(
85
- {
86
- slug: 'analysis',
87
- runIf: {
88
- detectLanguage: (result) =>
89
- result.language === input.run.preferredLanguage ||
90
- !input.run.preferredLanguage,
91
- },
92
- },
93
- new Flow<string>({ slug: 'yolo ' })
94
- .step(
95
- { slug: 'answerDirect' },
96
- async (input) =>
97
- await generateAnswer(input.run.query, input.detectLanguage.language)
98
- )
99
- .step(
100
- { slug: 'answerDirect' },
101
- async (input) =>
102
- await generateAnswer(input.run.query, input.detectLanguage.language)
103
- )
104
- )
105
- .step(
106
- {
107
- slug: 'formatOutput',
108
- dependsOn: ['translation.answerTranslated', 'directAnswer.answerDirect'],
109
- optional: ['translation.answerTranslated', 'directAnswer.answerDirect'],
110
- },
111
- async (input) => {
112
- const answerData =
113
- input.translation?.answerTranslated || input.directAnswer?.answerDirect;
114
- return await formatResponse(answerData.answer, answerData.sources);
115
- }
116
- )
117
- .step(
118
- { slug: 'logResponse', dependsOn: ['formatOutput'] },
119
- async (input) =>
120
- await logResult({
121
- query: input.run.query,
122
- response: input.formatOutput.formattedResponse,
123
- })
124
- );
@@ -1,83 +0,0 @@
1
- import { Flow } from './dsl.ts';
2
-
3
- // Define the input type for our flow
4
- type Input = {
5
- query: string;
6
- preferredLanguage?: string;
7
- };
8
-
9
- // Using the new branch DSL method
10
- export const QueryAnswerFlow = new Flow<Input>({
11
- slug: 'query_answer',
12
- maxAttempts: 3,
13
- baseDelay: 5,
14
- timeout: 60,
15
- })
16
- .step(
17
- { slug: 'detectLanguage' },
18
- async (input) => await detectLanguage(input.run.query)
19
- )
20
- .branch(
21
- {
22
- slug: 'translation',
23
- runIf: {
24
- detectLanguage: (result) =>
25
- result.language !== input.run.preferredLanguage &&
26
- input.run.preferredLanguage,
27
- },
28
- },
29
- (branch) =>
30
- branch
31
- .step(
32
- { slug: 'translate' },
33
- async (input) =>
34
- await translateText(
35
- input.run.query,
36
- input.run.preferredLanguage || 'english'
37
- )
38
- )
39
- .step(
40
- { slug: 'answerTranslated' },
41
- async (input) =>
42
- await generateAnswer(
43
- input.translate.translatedText,
44
- input.run.preferredLanguage || 'english'
45
- )
46
- )
47
- )
48
- .branch(
49
- {
50
- slug: 'directAnswer',
51
- runIf: {
52
- detectLanguage: (result) =>
53
- result.language === input.run.preferredLanguage ||
54
- !input.run.preferredLanguage,
55
- },
56
- },
57
- (branch) =>
58
- branch.step(
59
- { slug: 'answerDirect' },
60
- async (input) =>
61
- await generateAnswer(input.run.query, input.detectLanguage.language)
62
- )
63
- )
64
- .step(
65
- {
66
- slug: 'formatOutput',
67
- dependsOn: ['translation.answerTranslated', 'directAnswer.answerDirect'],
68
- optional: ['translation.answerTranslated', 'directAnswer.answerDirect'],
69
- },
70
- async (input) => {
71
- const answerData =
72
- input.translation?.answerTranslated || input.directAnswer?.answerDirect;
73
- return await formatResponse(answerData.answer, answerData.sources);
74
- }
75
- )
76
- .step(
77
- { slug: 'logResponse', dependsOn: ['formatOutput'] },
78
- async (input) =>
79
- await logResult({
80
- query: input.run.query,
81
- response: input.formatOutput.formattedResponse,
82
- })
83
- );
@@ -1,119 +0,0 @@
1
- # Comparing Two Approaches to Subflows in the Context of Versioning
2
-
3
- This document analyzes two different ways of handling subflows (or “branching”) in a pgflow-like system and how they relate to versioning. We will refer to:
4
-
5
- 1. **Approach A**: Subflows are defined and stored as standalone `Flow` objects in the database, which can be referenced by parent flows.
6
- 2. **Approach B**: Subflows (or “branches”) are appended (flattened) immediately into the parent flow in code, with slug-prefixing for isolation.
7
-
8
- We will compare these approaches, focusing especially on how flow versioning plays out in each scenario.
9
-
10
- ---
11
-
12
- ## Background on Flow Versioning
13
-
14
- pgflow’s versioning strategy is often approached by treating each flow definition as immutable once stored:
15
-
16
- - Each flow definition stays unchanged in the database once uploaded.
17
- - Changing a flow’s logic requires uploading a new flow slug (e.g., `myFlow_v2` instead of `myFlow_v1`).
18
- - This explicitly separates old runs from new runs.
19
- - It keeps flows simpler to reason about (no in-place updates that can lead to “half-upgraded” scenarios).
20
-
21
- Subflow usage under this scheme means that if the subflow’s logic or steps need to change:
22
- - Under Approach A, the user would upload a new version of the subflow flow slug.
23
- - Under Approach B, changes to the subflow are effectively changes to the parent flow itself (flattened in the final DAG).
24
-
25
- Below, we’ll walk through how each subflow approach interacts with this versioning model.
26
-
27
- ---
28
-
29
- ## Approach A: Standalone Subflows
30
-
31
- ### Description
32
-
33
- In this approach:
34
- 1. Each subflow is a standalone `Flow` with its own slug.
35
- 2. The parent flow references the subflow slug and sets up input/output mappings.
36
- 3. The engine links them at runtime by storing a reference (e.g., parent run/step → subflow run).
37
-
38
- Persistence-wise, subflows might store a `parent_run_id + parent_step_slug` in `pgflow.runs`, indicating that a subflow run is part of a higher-level run. Each subflow can also keep its own version slug.
39
-
40
- ### Versioning Impact
41
-
42
- - **Independent Versioning**
43
- Each subflow gets uploaded to the database under its own slug, say `payment_flow_v3`. If the subflow’s logic changes, you create `payment_flow_v4`. You then refer to `payment_flow_v4` from a new parent flow version if desired.
44
-
45
- - **Reusability**
46
- Multiple parent flows can reference the same subflow version or different versions of the subflow. For example, one parent flow might still use `payment_flow_v3`, while another references `payment_flow_v4`.
47
-
48
- - **Clear Boundaries**
49
- Because each subflow is its own entity with a slug, it is easier to see where the subflow boundary lies and which version is being invoked. You can introspect the table of flows to see exactly which subflows are used by each version of each parent flow.
50
-
51
- - **Overhead in Management**
52
- The main downside is that you must manage multiple database entries with potentially many flow slugs. In large systems, you might end up with many versions that you have to keep track of, along with their references.
53
-
54
- In short, Approach A fits neatly with pgflow’s “immutable upload” versioning style. Each subflow is simply another flow that is also immutable. Parent flows can upgrade independently by referencing new versions of their subflows.
55
-
56
- ---
57
-
58
- ## Approach B: Flattened “Branches”
59
-
60
- ### Description
61
-
62
- In this approach (sometimes called `.branch()` or `.subflow()` in the DSL):
63
-
64
- 1. You call a `.subflow()` (or `.branch()`) method on the parent flow.
65
- 2. That subflow’s steps are immediately added (flattened) into the parent flow’s step list.
66
- 3. Within the final DAG, each subflow step slug is internally prefixed (e.g., `branch1.some_step`) to avoid collisions.
67
- 4. The subflow’s relative dependencies remain consistent, but it’s all stored and tracked as a single flow in the database.
68
-
69
- Visually, you still see a subflow in code, but from the database perspective, it’s just one set of steps belonging to the parent flow, each slug forcibly prefixed.
70
-
71
- ### Versioning Impact
72
-
73
- - **Single Flow Slug**
74
- Because the subflow is flattened, you end up with a single flow slug representing the entire DAG. If you need to update the subflow steps, you’re effectively changing the entire parent flow. This means you deploy a new version of the parent flow (e.g., `parent_flow_v2`) to incorporate changed “subflow” steps.
75
-
76
- - **No Independent Subflow Versions**
77
- There isn’t a separate, independently tracked `Flow` object in the database. You cannot keep a stable “payment_flow_v3” that is reused, because it is not actually stored as its own entity. It is always merged into the parent when you `.subflow(...)`.
78
-
79
- - **Simplicity in Code**
80
- From the user’s perspective in TypeScript code, it can be very straightforward: you just define a “branch” in the function syntax. There’s no separate “upload the subflow flow” step.
81
- However, from a pure versioning viewpoint, it’s less flexible. You can’t upgrade the subflow independently—any subflow change triggers a new *parent flow version*.
82
-
83
- - **Potential Duplication**
84
- If multiple parent flows want the same subflow logic, each (parent) flow is storing that subflow’s steps. This can lead to duplication across multiple flows (and multiple places to update if you want consistency among them).
85
-
86
- ---
87
-
88
- ## Which Approach Is Easier to Use with Immutable Versioning?
89
-
90
- It depends on your needs:
91
-
92
- 1. **Approach A (Standalone Subflows)**
93
- - **Pros**:
94
- - One subflow flow slug can be easily versioned and referenced by multiple parents.
95
- - Easier to see which version of the subflow is used.
96
- - Ideal if you need a truly reusable piece of logic across many flows.
97
- - **Cons**:
98
- - Requires managing references between parent flow versions and subflow versions.
99
- - Slightly more overhead in “flow slug management” in the DB.
100
-
101
- 2. **Approach B (Flattened Branches)**
102
- - **Pros**:
103
- - Simpler to read or write in code, as you “just add steps” without referencing a second flow.
104
- - No special subflow DB entries or separate subflow run records.
105
- - **Cons**:
106
- - Not reusable as a separate entity (changing subflow requires a new version of the entire parent flow).
107
- - Potential duplication if the same subflow logic is used across many flows.
108
- - Harder to maintain an organized version history of subflows because each subflow is “encapsulated” within a parent flow version.
109
-
110
- From a purely versioning standpoint, **Approach A** aligns more directly with pgflow’s principle of immutable flows: You can create and track separate versions of the subflow and clearly see which parent flows reference them. **Approach B** remains attractive for simpler or “one-off” subflows that don’t need to be used anywhere else.
111
-
112
- ---
113
-
114
- ## Conclusion
115
-
116
- - If you want robust, maintainable versioning paths for subflows—where the same subflow logic might change independently and be referenced by multiple parents—Approach A is likely better. You will have a more explicit, consistent way to handle updates across multiple flows.
117
- - If your subflow is more like a local branch logic snippet, needed only by that parent flow, and you don’t expect to reuse it, Approach B can be simpler. You’ll still keep an immutable “parent flow version,” but you won’t have to track a separate subflow slug.
118
-
119
- Ultimately, your choice depends on how often you expect subflows to be reused, how critical their independent versioning is, and whether you want the subflows to appear as first-class flows in the database. For official “reusable building blocks,” a separate flow slug per subflow (Approach A) is typically the most consistent with a versioned, immutable database model.
@@ -1,138 +0,0 @@
1
- # Subflows in pgflow: Comparing Two Approaches
2
-
3
- This document compares two different ways of implementing *subflows* (or *branching*) within **pgflow**. Specifically, we look at:
4
-
5
- 1. **Standalone Subflows**
6
- - As described in `subflows.md`, subflows are stored as separate flows in the database.
7
- - They are referenced by special DSL methods and require separate input/output mapping.
8
- - The database would need a particular column (e.g., `parent_run_id` + `step_slug`) in `pgflow.runs` to track a subflow’s parent.
9
-
10
- 2. **Flattened Branches (`.branch()` or `.subflow()`)**
11
- - Steps are “immediately appended” to the main flow but grouped logically as a “branch.”
12
- - Step slugs in the branch receive an automatic prefix to avoid name collisions.
13
- - Subflows are effectively “flattened” into the parent, but the DSL can preserve isolation through prefixing.
14
- - An alternate design might allow passing an entire `Flow` object into `.subflow()`, flattening and prefixing each step under a single slug.
15
-
16
- Below, we analyze these two approaches based on readability, maintainability, ease of implementation, schema changes, and performance.
17
-
18
- ---
19
-
20
- ## 1. Standalone Subflows
21
-
22
- ### Description
23
- - Each subflow is defined as its own flow in the DSL and written to the database as an independent entity.
24
- - A parent flow that wants to invoke a subflow references it by some special method, passing input and receiving output.
25
- - Internally, the database must store extra *relationship fields*, such as `parent_run_id` or `parent_step_slug`.
26
-
27
- ### Key Points
28
- - Subflow runs have their own records in the `runs` table (with a field linking them back to the parent run/step).
29
- - Each subflow can be reused by referencing it from multiple parent flows without redefinition.
30
- - Input/output mapping between parent flow and subflow is explicit and can be edited independently.
31
-
32
- ---
33
-
34
- ## 2. Flattened Branches (`.branch()` or `.subflow()`)
35
-
36
- ### Description
37
- - A separate “branch” object is created in the DSL, but in reality, the steps are appended (flattened) to the main flow at definition time.
38
- - Steps inside the branch are prefixed with the branch slug to avoid name collisions (e.g., `branch1.some_step`).
39
- - The “branch” functionality is a DSL abstraction. It allows a more contained experience in code (like a local subflow), but physically they remain within a single flow run and single DAG in the database.
40
- - The `.branch()` method could accept a callback defining the sub-steps, or it could accept a `Flow` object and flatten it in place.
41
-
42
- ### Key Points
43
- - Maintains a single run record in the database: no new `runs` record for each subflow, because everything is part of the parent run.
44
- - No need for extra columns for parent-child run relationships; the logical subflow is merely a DSL concept.
45
- - Each step is stored under a prefixed slug (e.g., `branch_slug.step_slug`) to isolate dependencies and outputs within the parent flow.
46
-
47
- ---
48
-
49
- ## Comparison
50
-
51
- ### 1. Readability
52
-
53
- **Standalone Subflows**
54
- - Potentially clearer when you want a truly modular flow.
55
- - A subflow can be understood in isolation, with its own steps, dependencies, and run data.
56
- - References to subflows might look more verbose if you have multiple subflows in a single parent flow.
57
-
58
- **Flattened Branches**
59
- - The DSL usage can be very direct; you add a `.branch()` call inline, which shows the sub-steps right there in the code.
60
- - The flow remains conceptually “unified” from top to bottom, although the prefixed step slugs might be less direct if you are reading the flattened representation in the database.
61
- - Possibly simpler to grasp as a quick branching or subflow without large structural changes.
62
-
63
- **Verdict**:
64
- - If you value the ability to treat subflows as self-contained “mini-flows,” free of the parent’s step definitions, the standalone approach is more explicit.
65
- - If you prefer everything in one place, flattened branches offer a straightforward inline style.
66
-
67
- ### 2. Maintainability
68
-
69
- **Standalone Subflows**
70
- - Fits well if you plan to reuse subflows in multiple places. You maintain the subflow in a separate, self-contained file or DSL definition.
71
- - Changes to the subflow do not directly break the parent flow’s definitions (as long as the input/output interface remains compatible).
72
- - Potential overhead in updating any references in the parent flows if you change the I/O signature of the subflow.
73
-
74
- **Flattened Branches**
75
- - Changing or removing a branch typically just changes the portion of the code that defines that segment of the flow.
76
- - The steps get stored as if they were part of the main flow, so you do not have an extra subflow entity to manage or version.
77
- - Not as easily reusable if you want the same subflow logic in multiple parent flows (though you could replicate the `.branch()` code or attempt a DSL function).
78
-
79
- **Verdict**:
80
- - Standalone subflows can be more reusable and can reduce duplication in large codebases.
81
- - Flattened branches are easier to keep contained within a single flow’s definition, however, and maintain a simpler mental model if you rarely need to share subflows.
82
-
83
- ### 3. Ease of Implementation
84
-
85
- **Standalone Subflows**
86
- - Requires additional concepts at the database layer: a subflow run must carry references to its parent run/step.
87
- - You need new logic in the engine to start, link, and complete these subflow runs.
88
- - The DSL and engine must handle mapping inputs/outputs across flow boundaries.
89
-
90
- **Flattened Branches**
91
- - Simpler from a runtime perspective, since everything is just one flow run.
92
- - The DSL logic that “flattens” the subflow steps is not overly complex; it mostly handles prefixing slugs and hooking up dependencies.
93
- - No new database columns or specialized logic for multiple runs.
94
-
95
- **Verdict**:
96
- - Flattened branches require fewer structural changes and might be quicker to implement.
97
- - True subflow runs demand a more robust solution at both the DSL and SQL layer.
98
-
99
- ### 4. Impact on SQL Schema / Migration Cost
100
-
101
- **Standalone Subflows**
102
- - Necessitates additional database columns or relationships. For instance, the `runs` table needs to store `parent_run_id`, `parent_step_slug`, or something similar to link subflow runs to their parent.
103
- - Removing or changing this schema later may be more involved if it’s deeply integrated into how runs are tracked.
104
-
105
- **Flattened Branches**
106
- - No major schema changes are required—still the same approach of storing steps, runs, step states, etc.
107
- - You only introduce new step slugs that happen to include a “branch prefix” for uniqueness.
108
- - Easier to remove or refactor because it doesn’t alter the overarching structure of the data model.
109
-
110
- **Verdict**:
111
- - Standalone subflows require more schema modifications, making them more complex to revert or adjust.
112
- - Flattened branches have minimal, if any, schema changes.
113
-
114
- ### 5. Performance Impact
115
-
116
- **Standalone Subflows**
117
- - Potentially more overhead spinning up multiple runs, especially if subflows are large or run in parallel.
118
- - Each subflow has its own overhead (e.g., run record, root step tasks, etc.). However, for large workflows, this might help isolate concurrency or error handling at the subflow boundary.
119
-
120
- **Flattened Branches**
121
- - The entire workflow, including “branch” steps, exists in a single run, so there is no extra overhead from creating new runs.
122
- - You treat all steps as part of one DAG, so concurrency and step scheduling is handled in one place. This can be more efficient if the flows share certain resources or data.
123
- - For extremely large flows, flattening might produce an enormous DAG, which could be less clear or hamper debugging if you tend to think in separate modules.
124
-
125
- **Verdict**:
126
- - Flattened branches are simpler to schedule in a single run, with presumably lower overhead for short or mid-sized flows.
127
- - If your subflows are big, standalone subflows might be beneficial from a concurrency and debugging standpoint, but they also mean more “runs” exist in the database.
128
-
129
- ---
130
-
131
- ## Conclusion
132
-
133
- Both approaches solve the problem of reusing and modularizing flow logic. The right choice depends on how you plan to structure and reuse workflows:
134
-
135
- - **Standalone Subflows** are powerful if you need fully independent runs or if reusability and clear boundaries between subflows are critical. On the other hand, they complicate the SQL schema and add overhead in linking parent and child runs.
136
- - **Flattened Branches** (or `.subflow()`) allow you to keep everything in one run and maintain a simpler database structure. This is typically easier to implement and manage, especially if you don’t need separate subflow runs or advanced usage patterns. However, it offers less natural reuse if you need to embed the same subflow logic in multiple parent flows without duplication.
137
-
138
- In practice, **flattened branches** often suffice for many workflow needs. If your application demands heavy reuse of subflows across many flows, or if you need logically distinct runs for separate modules, **standalone subflows** might be the more scalable approach.
@@ -1,118 +0,0 @@
1
- # Implementing Subflows in pgflow
2
-
3
- Subflows allow you to create reusable workflow components while maintaining type safety and a clean, declarative syntax. This document outlines how to implement subflows using a type-safe field mapping approach.
4
-
5
- ## How Subflows Work
6
-
7
- A subflow is a complete workflow that is embedded within another workflow. The parent flow must:
8
-
9
- 1. Map its own inputs/outputs to the subflow's required inputs
10
- 2. Access the subflow's outputs in subsequent steps
11
-
12
- ## Implementation Example
13
-
14
- Let's look at a practical example with a payment processing subflow that's used within an order processing workflow.
15
-
16
- ### Step 1: Define the Payment Flow
17
-
18
- First, we define our reusable payment processing flow:
19
-
20
- ```typescript
21
- // Define a payment processing flow
22
- const PaymentFlow = new Flow<{
23
- orderId: string;
24
- amount: number;
25
- currency: string;
26
- }>({
27
- slug: 'payment_flow',
28
- })
29
- .step({ slug: 'validate_payment' }, async (input) => {
30
- return validatePayment(
31
- input.run.orderId,
32
- input.run.amount,
33
- input.run.currency
34
- );
35
- // Returns { validationToken: string, isValid: boolean }
36
- })
37
- .step(
38
- { slug: 'process_transaction', dependsOn: ['validate_payment'] },
39
- async (input) => {
40
- if (!input.validate_payment.isValid) {
41
- throw new Error('Payment validation failed');
42
- }
43
- return processTransaction(
44
- input.run.orderId,
45
- input.run.amount,
46
- input.validate_payment.validationToken
47
- );
48
- // Returns { transactionId: string, status: string, timestamp: Date }
49
- }
50
- );
51
- ```
52
-
53
- ### Step 2: Use the Payment Flow as a Subflow
54
-
55
- Now we can use this payment flow as a subflow in our order processing workflow:
56
-
57
- ```typescript
58
- // Use PaymentFlow as a subflow in an order processing flow
59
- const OrderFlow = new Flow<{ customerId: string; items: CartItem[] }>({
60
- slug: 'order_flow',
61
- })
62
- .step({ slug: 'create_order' }, async (input) => {
63
- return createOrder(input.run.customerId, input.run.items);
64
- // Returns { orderId: string, totalAmount: number, currency: string }
65
- })
66
- .subflow({
67
- slug: 'process_payment',
68
- dependsOn: ['create_order'],
69
- flow: PaymentFlow,
70
- input: (input) => ({
71
- orderId: input.create_order.orderId,
72
- amount: input.create_order.totalAmount,
73
- currency: input.create_order.currency || 'USD',
74
- }),
75
- output: (subflowOutput) => ({
76
- transactionId: subflowOutput.process_transaction.transactionId,
77
- paymentStatus: subflowOutput.process_transaction.status,
78
- paymentTimestamp: subflowOutput.process_transaction.timestamp,
79
- }),
80
- })
81
- .step(
82
- { slug: 'finalize_order', dependsOn: ['process_payment'] },
83
- async (input) => {
84
- return finalizeOrder(
85
- input.create_order.orderId,
86
- input.process_payment.transactionId,
87
- input.process_payment.paymentStatus
88
- );
89
- // Returns { orderStatus: string, completionDate: Date }
90
- }
91
- );
92
- ```
93
-
94
- ## Key Properties
95
-
96
- The `subflow()` method accepts:
97
-
98
- - `slug`: A unique identifier for the subflow step
99
- - `dependsOn`: The steps that must complete before this subflow can be executed
100
- - `flow`: The Flow object to be executed as a subflow
101
- - `input`: A function that maps parent flow data to the subflow's input requirements
102
- - `output`: A function that transforms the subflow's output for easier consumption by subsequent steps
103
-
104
- ## Type Safety
105
-
106
- The entire subflow implementation maintains full type safety:
107
-
108
- 1. The `input` function is type-checked against the subflow's input requirements
109
- 2. The `output` function is type-checked against the subflow's available outputs
110
- 3. Subsequent steps in the parent flow can access the transformed subflow outputs with full type information
111
-
112
- ## Benefits
113
-
114
- - **Reusability**: Create workflow components once and reuse them across multiple flows
115
- - **Maintainability**: Update a subflow in one place and all parent flows benefit
116
- - **Clean API**: The input/output mappings provide a clean interface between workflows
117
- - **Type Safety**: Maintain full type checking across flow boundaries
118
- - **Transparency**: The execution engine handles subflows seamlessly, making them appear as normal steps to users