soe-ai 0.1.0__py3-none-any.whl → 0.1.2__py3-none-any.whl
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.
- soe/broker.py +4 -5
- soe/builtin_tools/__init__.py +39 -0
- soe/builtin_tools/soe_add_signal.py +82 -0
- soe/builtin_tools/soe_call_tool.py +111 -0
- soe/builtin_tools/soe_copy_context.py +80 -0
- soe/builtin_tools/soe_explore_docs.py +290 -0
- soe/builtin_tools/soe_get_available_tools.py +42 -0
- soe/builtin_tools/soe_get_context.py +50 -0
- soe/builtin_tools/soe_get_workflows.py +63 -0
- soe/builtin_tools/soe_inject_node.py +86 -0
- soe/builtin_tools/soe_inject_workflow.py +105 -0
- soe/builtin_tools/soe_list_contexts.py +73 -0
- soe/builtin_tools/soe_remove_node.py +72 -0
- soe/builtin_tools/soe_remove_workflow.py +62 -0
- soe/builtin_tools/soe_update_context.py +54 -0
- soe/docs/_config.yml +10 -0
- soe/docs/advanced_patterns/guide_fanout_and_aggregations.md +318 -0
- soe/docs/advanced_patterns/guide_inheritance.md +435 -0
- soe/docs/advanced_patterns/hybrid_intelligence.md +237 -0
- soe/docs/advanced_patterns/index.md +49 -0
- soe/docs/advanced_patterns/operational.md +781 -0
- soe/docs/advanced_patterns/self_evolving_workflows.md +385 -0
- soe/docs/advanced_patterns/swarm_intelligence.md +211 -0
- soe/docs/builtins/context.md +164 -0
- soe/docs/builtins/explore_docs.md +135 -0
- soe/docs/builtins/tools.md +164 -0
- soe/docs/builtins/workflows.md +199 -0
- soe/docs/guide_00_getting_started.md +341 -0
- soe/docs/guide_01_tool.md +206 -0
- soe/docs/guide_02_llm.md +143 -0
- soe/docs/guide_03_router.md +146 -0
- soe/docs/guide_04_patterns.md +475 -0
- soe/docs/guide_05_agent.md +159 -0
- soe/docs/guide_06_schema.md +397 -0
- soe/docs/guide_07_identity.md +540 -0
- soe/docs/guide_08_child.md +612 -0
- soe/docs/guide_09_ecosystem.md +690 -0
- soe/docs/guide_10_infrastructure.md +427 -0
- soe/docs/guide_11_builtins.md +118 -0
- soe/docs/index.md +104 -0
- soe/docs/primitives/backends.md +281 -0
- soe/docs/primitives/context.md +256 -0
- soe/docs/primitives/node_reference.md +259 -0
- soe/docs/primitives/primitives.md +331 -0
- soe/docs/primitives/signals.md +865 -0
- soe/docs_index.py +1 -1
- soe/init.py +2 -2
- soe/lib/__init__.py +0 -0
- soe/lib/child_context.py +46 -0
- soe/lib/context_fields.py +51 -0
- soe/lib/inheritance.py +172 -0
- soe/lib/jinja_render.py +113 -0
- soe/lib/operational.py +51 -0
- soe/lib/parent_sync.py +71 -0
- soe/lib/register_event.py +75 -0
- soe/lib/schema_validation.py +134 -0
- soe/lib/yaml_parser.py +14 -0
- soe/local_backends/__init__.py +18 -0
- soe/local_backends/factory.py +124 -0
- soe/local_backends/in_memory/context.py +38 -0
- soe/local_backends/in_memory/conversation_history.py +60 -0
- soe/local_backends/in_memory/identity.py +52 -0
- soe/local_backends/in_memory/schema.py +40 -0
- soe/local_backends/in_memory/telemetry.py +38 -0
- soe/local_backends/in_memory/workflow.py +33 -0
- soe/local_backends/storage/context.py +57 -0
- soe/local_backends/storage/conversation_history.py +82 -0
- soe/local_backends/storage/identity.py +118 -0
- soe/local_backends/storage/schema.py +96 -0
- soe/local_backends/storage/telemetry.py +72 -0
- soe/local_backends/storage/workflow.py +56 -0
- soe/nodes/__init__.py +13 -0
- soe/nodes/agent/__init__.py +10 -0
- soe/nodes/agent/factory.py +134 -0
- soe/nodes/agent/lib/loop_handlers.py +150 -0
- soe/nodes/agent/lib/loop_state.py +157 -0
- soe/nodes/agent/lib/prompts.py +65 -0
- soe/nodes/agent/lib/tools.py +35 -0
- soe/nodes/agent/stages/__init__.py +12 -0
- soe/nodes/agent/stages/parameter.py +37 -0
- soe/nodes/agent/stages/response.py +54 -0
- soe/nodes/agent/stages/router.py +37 -0
- soe/nodes/agent/state.py +111 -0
- soe/nodes/agent/types.py +66 -0
- soe/nodes/agent/validation/__init__.py +11 -0
- soe/nodes/agent/validation/config.py +95 -0
- soe/nodes/agent/validation/operational.py +24 -0
- soe/nodes/child/__init__.py +3 -0
- soe/nodes/child/factory.py +61 -0
- soe/nodes/child/state.py +59 -0
- soe/nodes/child/validation/__init__.py +11 -0
- soe/nodes/child/validation/config.py +126 -0
- soe/nodes/child/validation/operational.py +28 -0
- soe/nodes/lib/conditions.py +71 -0
- soe/nodes/lib/context.py +24 -0
- soe/nodes/lib/conversation_history.py +77 -0
- soe/nodes/lib/identity.py +64 -0
- soe/nodes/lib/llm_resolver.py +142 -0
- soe/nodes/lib/output.py +68 -0
- soe/nodes/lib/response_builder.py +91 -0
- soe/nodes/lib/signal_emission.py +79 -0
- soe/nodes/lib/signals.py +54 -0
- soe/nodes/lib/tools.py +100 -0
- soe/nodes/llm/__init__.py +7 -0
- soe/nodes/llm/factory.py +103 -0
- soe/nodes/llm/state.py +76 -0
- soe/nodes/llm/types.py +12 -0
- soe/nodes/llm/validation/__init__.py +11 -0
- soe/nodes/llm/validation/config.py +89 -0
- soe/nodes/llm/validation/operational.py +23 -0
- soe/nodes/router/__init__.py +3 -0
- soe/nodes/router/factory.py +37 -0
- soe/nodes/router/state.py +32 -0
- soe/nodes/router/validation/__init__.py +11 -0
- soe/nodes/router/validation/config.py +58 -0
- soe/nodes/router/validation/operational.py +16 -0
- soe/nodes/tool/factory.py +66 -0
- soe/nodes/tool/lib/__init__.py +11 -0
- soe/nodes/tool/lib/conditions.py +35 -0
- soe/nodes/tool/lib/failure.py +28 -0
- soe/nodes/tool/lib/parameters.py +67 -0
- soe/nodes/tool/state.py +66 -0
- soe/nodes/tool/types.py +27 -0
- soe/nodes/tool/validation/__init__.py +15 -0
- soe/nodes/tool/validation/config.py +132 -0
- soe/nodes/tool/validation/operational.py +16 -0
- soe/types.py +40 -28
- soe/validation/__init__.py +18 -0
- soe/validation/config.py +195 -0
- soe/validation/jinja.py +54 -0
- soe/validation/operational.py +110 -0
- {soe_ai-0.1.0.dist-info → soe_ai-0.1.2.dist-info}/METADATA +72 -9
- soe_ai-0.1.2.dist-info/RECORD +137 -0
- {soe_ai-0.1.0.dist-info → soe_ai-0.1.2.dist-info}/WHEEL +1 -1
- soe/validation.py +0 -8
- soe_ai-0.1.0.dist-info/RECORD +0 -11
- {soe_ai-0.1.0.dist-info → soe_ai-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {soe_ai-0.1.0.dist-info → soe_ai-0.1.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
|
|
2
|
+
# SOE Guide: Chapter 9 - The Workflows Ecosystem
|
|
3
|
+
|
|
4
|
+
## Understanding the Big Picture
|
|
5
|
+
|
|
6
|
+
Before diving deeper into SOE, it's important to understand **why** workflows are structured the way they are. This chapter explains the ecosystem—how workflows relate to each other, how data persists, and how you can build sophisticated multi-workflow systems.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Combined Config: The Recommended Pattern
|
|
11
|
+
|
|
12
|
+
The most powerful way to configure SOE is with **combined config**—a single structure containing workflows, context schemas, and identities:
|
|
13
|
+
|
|
14
|
+
```yaml
|
|
15
|
+
workflows:
|
|
16
|
+
example_workflow:
|
|
17
|
+
Analyze:
|
|
18
|
+
node_type: llm
|
|
19
|
+
event_triggers: [START]
|
|
20
|
+
identity: analyst
|
|
21
|
+
prompt: "Analyze: {{ context.input }}"
|
|
22
|
+
output_field: analysis
|
|
23
|
+
event_emissions:
|
|
24
|
+
- signal_name: ANALYZED
|
|
25
|
+
|
|
26
|
+
Summarize:
|
|
27
|
+
node_type: llm
|
|
28
|
+
event_triggers: [ANALYZED]
|
|
29
|
+
identity: analyst
|
|
30
|
+
prompt: "Summarize the analysis: {{ context.analysis }}"
|
|
31
|
+
output_field: summary
|
|
32
|
+
event_emissions:
|
|
33
|
+
- signal_name: DONE
|
|
34
|
+
|
|
35
|
+
context_schema:
|
|
36
|
+
input:
|
|
37
|
+
type: string
|
|
38
|
+
description: The input to analyze
|
|
39
|
+
analysis:
|
|
40
|
+
type: object
|
|
41
|
+
description: Detailed analysis result
|
|
42
|
+
summary:
|
|
43
|
+
type: string
|
|
44
|
+
description: A concise summary
|
|
45
|
+
|
|
46
|
+
identities:
|
|
47
|
+
analyst: "You are a thorough analyst. Be precise and structured in your analysis."
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Why Combined Config?
|
|
51
|
+
|
|
52
|
+
1. **Single source of truth**: All configuration in one place
|
|
53
|
+
2. **Automatic setup**: Schemas and identities are saved to backends automatically
|
|
54
|
+
3. **Keyed by execution**: Child workflows can access parent's schemas and identities
|
|
55
|
+
4. **Clear structure**: Easy to understand what your workflow ecosystem contains
|
|
56
|
+
|
|
57
|
+
### Combined Config Sections
|
|
58
|
+
|
|
59
|
+
| Section | Purpose | Required |
|
|
60
|
+
|---------|---------|----------|
|
|
61
|
+
| `workflows` | Workflow definitions (nodes, signals) | Yes |
|
|
62
|
+
| `context_schema` | Field type validation for LLM outputs | No |
|
|
63
|
+
| `identities` | System prompts for LLM/Agent nodes | No |
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Multi-Workflow Ecosystems
|
|
68
|
+
|
|
69
|
+
When you call `orchestrate()`, you pass a **config**. This config can contain multiple workflow definitions:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
execution_id = orchestrate(
|
|
73
|
+
config=my_config, # Contains MULTIPLE workflows
|
|
74
|
+
initial_workflow_name="main_workflow", # Which one to start
|
|
75
|
+
initial_signals=["START"],
|
|
76
|
+
initial_context={...},
|
|
77
|
+
backends=backends,
|
|
78
|
+
broadcast_signals_caller=broadcast,
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Why Multiple Workflows?
|
|
83
|
+
|
|
84
|
+
The config is your **ecosystem**. Workflows can:
|
|
85
|
+
|
|
86
|
+
1. **Spawn children** — One workflow triggers another via child nodes
|
|
87
|
+
2. **Share definitions** — Common sub-workflows reused across your system
|
|
88
|
+
3. **Run in parallel** — Multiple child workflows executing simultaneously
|
|
89
|
+
4. **Share schemas and identities** — Via `main_execution_id` keying
|
|
90
|
+
|
|
91
|
+
### Example: Full Ecosystem Config
|
|
92
|
+
|
|
93
|
+
```yaml
|
|
94
|
+
workflows:
|
|
95
|
+
main_workflow:
|
|
96
|
+
Classifier:
|
|
97
|
+
node_type: router
|
|
98
|
+
event_triggers: [START]
|
|
99
|
+
event_emissions:
|
|
100
|
+
- signal_name: HANDLE_TEXT
|
|
101
|
+
condition: "{{ context.input_type == 'text' }}"
|
|
102
|
+
- signal_name: HANDLE_IMAGE
|
|
103
|
+
condition: "{{ context.input_type == 'image' }}"
|
|
104
|
+
|
|
105
|
+
DelegateToTextProcessor:
|
|
106
|
+
node_type: child
|
|
107
|
+
event_triggers: [HANDLE_TEXT]
|
|
108
|
+
child_workflow_name: text_workflow
|
|
109
|
+
child_initial_signals: [START]
|
|
110
|
+
input_fields: [content]
|
|
111
|
+
signals_to_parent: [DONE]
|
|
112
|
+
context_updates_to_parent: [result]
|
|
113
|
+
event_emissions:
|
|
114
|
+
- signal_name: PROCESSING_COMPLETE
|
|
115
|
+
|
|
116
|
+
DelegateToImageProcessor:
|
|
117
|
+
node_type: child
|
|
118
|
+
event_triggers: [HANDLE_IMAGE]
|
|
119
|
+
child_workflow_name: image_workflow
|
|
120
|
+
child_initial_signals: [START]
|
|
121
|
+
input_fields: [content]
|
|
122
|
+
signals_to_parent: [DONE]
|
|
123
|
+
context_updates_to_parent: [result]
|
|
124
|
+
event_emissions:
|
|
125
|
+
- signal_name: PROCESSING_COMPLETE
|
|
126
|
+
|
|
127
|
+
Finalize:
|
|
128
|
+
node_type: router
|
|
129
|
+
event_triggers: [PROCESSING_COMPLETE]
|
|
130
|
+
event_emissions:
|
|
131
|
+
- signal_name: COMPLETE
|
|
132
|
+
|
|
133
|
+
text_workflow:
|
|
134
|
+
AnalyzeText:
|
|
135
|
+
node_type: llm
|
|
136
|
+
event_triggers: [START]
|
|
137
|
+
identity: text_analyzer
|
|
138
|
+
prompt: "Analyze this text: {{ context.content }}"
|
|
139
|
+
output_field: result
|
|
140
|
+
event_emissions:
|
|
141
|
+
- signal_name: DONE
|
|
142
|
+
|
|
143
|
+
image_workflow:
|
|
144
|
+
AnalyzeImage:
|
|
145
|
+
node_type: llm
|
|
146
|
+
event_triggers: [START]
|
|
147
|
+
identity: image_analyzer
|
|
148
|
+
prompt: "Describe this image: {{ context.content }}"
|
|
149
|
+
output_field: result
|
|
150
|
+
event_emissions:
|
|
151
|
+
- signal_name: DONE
|
|
152
|
+
|
|
153
|
+
context_schema:
|
|
154
|
+
content:
|
|
155
|
+
type: string
|
|
156
|
+
description: The input content to process
|
|
157
|
+
result:
|
|
158
|
+
type: object
|
|
159
|
+
description: The processing result
|
|
160
|
+
|
|
161
|
+
identities:
|
|
162
|
+
text_analyzer: "You are an expert text analyst. Provide detailed, structured analysis."
|
|
163
|
+
image_analyzer: "You are an expert image analyst. Describe visual elements precisely."
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
In this example:
|
|
167
|
+
- `main_workflow` is the entry point that routes to specialized workflows
|
|
168
|
+
- `text_workflow` and `image_workflow` are child workflows
|
|
169
|
+
- Both child workflows share identities defined in the config
|
|
170
|
+
- Schema validation ensures consistent data structures
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Data vs Execution: A Critical Distinction
|
|
175
|
+
|
|
176
|
+
SOE separates two concerns:
|
|
177
|
+
|
|
178
|
+
| Concept | What It Is | Where It Lives |
|
|
179
|
+
|---------|------------|----------------|
|
|
180
|
+
| **Workflow Definition** | The YAML describing nodes and signals | `WorkflowBackend` |
|
|
181
|
+
| **Workflow Execution** | The running state of a specific run | `ContextBackend` |
|
|
182
|
+
|
|
183
|
+
### Workflow Definitions Are Data
|
|
184
|
+
|
|
185
|
+
Your workflow YAML is **just data**. It can be:
|
|
186
|
+
|
|
187
|
+
- Stored in files, databases, or version control
|
|
188
|
+
- Loaded dynamically at runtime
|
|
189
|
+
- Modified without restarting your application
|
|
190
|
+
- Versioned using any strategy you prefer
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
# Load from file
|
|
194
|
+
with open("workflows/my_workflow.yaml") as f:
|
|
195
|
+
workflow_yaml = f.read()
|
|
196
|
+
|
|
197
|
+
# Load from database
|
|
198
|
+
workflow_yaml = db.get_workflow("my_workflow", version="2.1.0")
|
|
199
|
+
|
|
200
|
+
# Pass to orchestrate
|
|
201
|
+
orchestrate(config=workflow_yaml, ...)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Executions Are Immutable
|
|
205
|
+
|
|
206
|
+
When you start an orchestration, the workflow definition is **captured** and stored in the `WorkflowBackend` for that execution. This means:
|
|
207
|
+
|
|
208
|
+
1. **No version conflicts**: If you update your workflow, existing executions continue with their original definition
|
|
209
|
+
2. **Natural versioning**: Each execution remembers which workflow version it started with
|
|
210
|
+
3. **Audit trail**: You can inspect what any historical execution ran
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
Execution 001 → Uses workflow v1.0 (stored in workflow backend)
|
|
214
|
+
Execution 002 → Uses workflow v1.1 (stored in workflow backend)
|
|
215
|
+
Execution 003 → Uses workflow v2.0 (stored in workflow backend)
|
|
216
|
+
|
|
217
|
+
↓ You update the workflow file ↓
|
|
218
|
+
|
|
219
|
+
Execution 001 → Still uses v1.0 (continues unchanged)
|
|
220
|
+
Execution 004 → Uses new version
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Why This Matters
|
|
224
|
+
|
|
225
|
+
This architecture means:
|
|
226
|
+
|
|
227
|
+
- **No downtime migrations**: Update workflows without stopping running processes
|
|
228
|
+
- **Rollback safety**: Bad workflow version? New executions use the old; existing continue
|
|
229
|
+
- **Debugging**: Inspect exactly what workflow an execution used, even months later
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Parallel Workflow Execution
|
|
234
|
+
|
|
235
|
+
Child workflows can run **in parallel**. When a router emits multiple signals and each triggers a child node, all children start simultaneously (in infrastructure that supports it):
|
|
236
|
+
|
|
237
|
+
### Fan-Out Pattern with Combined Config
|
|
238
|
+
|
|
239
|
+
```yaml
|
|
240
|
+
workflows:
|
|
241
|
+
orchestrator_workflow:
|
|
242
|
+
FanOut:
|
|
243
|
+
node_type: router
|
|
244
|
+
event_triggers: [START]
|
|
245
|
+
event_emissions:
|
|
246
|
+
- signal_name: START_WORKER_A
|
|
247
|
+
- signal_name: START_WORKER_B
|
|
248
|
+
- signal_name: START_WORKER_C
|
|
249
|
+
|
|
250
|
+
WorkerA:
|
|
251
|
+
node_type: child
|
|
252
|
+
event_triggers: [START_WORKER_A]
|
|
253
|
+
child_workflow_name: worker_workflow
|
|
254
|
+
child_initial_signals: [START]
|
|
255
|
+
input_fields: [chunk_a]
|
|
256
|
+
signals_to_parent: [WORKER_DONE]
|
|
257
|
+
context_updates_to_parent: [result_a]
|
|
258
|
+
event_emissions:
|
|
259
|
+
- signal_name: A_COMPLETE
|
|
260
|
+
|
|
261
|
+
WorkerB:
|
|
262
|
+
node_type: child
|
|
263
|
+
event_triggers: [START_WORKER_B]
|
|
264
|
+
child_workflow_name: worker_workflow
|
|
265
|
+
child_initial_signals: [START]
|
|
266
|
+
input_fields: [chunk_b]
|
|
267
|
+
signals_to_parent: [WORKER_DONE]
|
|
268
|
+
context_updates_to_parent: [result_b]
|
|
269
|
+
event_emissions:
|
|
270
|
+
- signal_name: B_COMPLETE
|
|
271
|
+
|
|
272
|
+
WorkerC:
|
|
273
|
+
node_type: child
|
|
274
|
+
event_triggers: [START_WORKER_C]
|
|
275
|
+
child_workflow_name: worker_workflow
|
|
276
|
+
child_initial_signals: [START]
|
|
277
|
+
input_fields: [chunk_c]
|
|
278
|
+
signals_to_parent: [WORKER_DONE]
|
|
279
|
+
context_updates_to_parent: [result_c]
|
|
280
|
+
event_emissions:
|
|
281
|
+
- signal_name: C_COMPLETE
|
|
282
|
+
|
|
283
|
+
Aggregate:
|
|
284
|
+
node_type: llm
|
|
285
|
+
event_triggers: [A_COMPLETE, B_COMPLETE, C_COMPLETE]
|
|
286
|
+
identity: aggregator
|
|
287
|
+
prompt: |
|
|
288
|
+
Aggregate the results:
|
|
289
|
+
- Result A: {{ context.result_a }}
|
|
290
|
+
- Result B: {{ context.result_b }}
|
|
291
|
+
- Result C: {{ context.result_c }}
|
|
292
|
+
output_field: final_result
|
|
293
|
+
event_emissions:
|
|
294
|
+
- signal_name: ALL_DONE
|
|
295
|
+
|
|
296
|
+
worker_workflow:
|
|
297
|
+
ProcessData:
|
|
298
|
+
node_type: llm
|
|
299
|
+
event_triggers: [START]
|
|
300
|
+
identity: data_processor
|
|
301
|
+
prompt: "Process this data chunk: {{ context.data }}"
|
|
302
|
+
output_field: result
|
|
303
|
+
event_emissions:
|
|
304
|
+
- signal_name: WORKER_DONE
|
|
305
|
+
|
|
306
|
+
context_schema:
|
|
307
|
+
chunk_a:
|
|
308
|
+
type: object
|
|
309
|
+
description: Data chunk for worker A
|
|
310
|
+
chunk_b:
|
|
311
|
+
type: object
|
|
312
|
+
description: Data chunk for worker B
|
|
313
|
+
chunk_c:
|
|
314
|
+
type: object
|
|
315
|
+
description: Data chunk for worker C
|
|
316
|
+
result_a:
|
|
317
|
+
type: object
|
|
318
|
+
description: Processing result from worker A
|
|
319
|
+
result_b:
|
|
320
|
+
type: object
|
|
321
|
+
description: Processing result from worker B
|
|
322
|
+
result_c:
|
|
323
|
+
type: object
|
|
324
|
+
description: Processing result from worker C
|
|
325
|
+
final_result:
|
|
326
|
+
type: object
|
|
327
|
+
description: Aggregated final result
|
|
328
|
+
|
|
329
|
+
identities:
|
|
330
|
+
data_processor: "You are a data processing specialist. Extract and transform data accurately."
|
|
331
|
+
aggregator: "You are an expert at synthesizing multiple data sources into coherent summaries."
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
This combined config includes:
|
|
335
|
+
- **workflows**: Orchestrator that fans out to workers
|
|
336
|
+
- **context_schema**: Validates data chunks and results
|
|
337
|
+
- **identities**: Specialized prompts for processor and aggregator
|
|
338
|
+
|
|
339
|
+
### How Parallel Execution Works
|
|
340
|
+
|
|
341
|
+
```
|
|
342
|
+
START
|
|
343
|
+
│
|
|
344
|
+
▼
|
|
345
|
+
┌─────────┐
|
|
346
|
+
│ FanOut │
|
|
347
|
+
│(Router) │
|
|
348
|
+
└────┬────┘
|
|
349
|
+
│
|
|
350
|
+
├──────────────┬──────────────┐
|
|
351
|
+
▼ ▼ ▼
|
|
352
|
+
┌─────────┐ ┌─────────┐ ┌─────────┐
|
|
353
|
+
│Worker A │ │Worker B │ │Worker C │
|
|
354
|
+
│ (Child) │ │ (Child) │ │ (Child) │
|
|
355
|
+
└────┬────┘ └────┬────┘ └────┬────┘
|
|
356
|
+
│ │ │
|
|
357
|
+
└──────────────┴──────────────┘
|
|
358
|
+
│
|
|
359
|
+
▼
|
|
360
|
+
┌───────────┐
|
|
361
|
+
│ Aggregate │
|
|
362
|
+
│ (Router) │
|
|
363
|
+
└───────────┘
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Each worker:
|
|
367
|
+
- Runs independently (potentially on different infrastructure)
|
|
368
|
+
- Updates its own context (isolated)
|
|
369
|
+
- Propagates results back via `context_updates_to_parent`
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## Fire-and-Forget vs Callbacks
|
|
374
|
+
|
|
375
|
+
Child workflows offer two patterns:
|
|
376
|
+
|
|
377
|
+
### Fire-and-Forget
|
|
378
|
+
|
|
379
|
+
Start a child and continue immediately—don't wait for completion:
|
|
380
|
+
|
|
381
|
+
```yaml
|
|
382
|
+
main_workflow:
|
|
383
|
+
LaunchBackground:
|
|
384
|
+
node_type: router
|
|
385
|
+
event_triggers: [START]
|
|
386
|
+
event_emissions:
|
|
387
|
+
- signal_name: TASK_LAUNCHED
|
|
388
|
+
- signal_name: START_BACKGROUND
|
|
389
|
+
|
|
390
|
+
StartBackgroundTask:
|
|
391
|
+
node_type: child
|
|
392
|
+
event_triggers: [START_BACKGROUND]
|
|
393
|
+
child_workflow_name: background_workflow
|
|
394
|
+
child_initial_signals: [START]
|
|
395
|
+
input_fields: [task_data]
|
|
396
|
+
# No signals_to_parent - we don't wait for completion
|
|
397
|
+
|
|
398
|
+
ContinueImmediately:
|
|
399
|
+
node_type: router
|
|
400
|
+
event_triggers: [TASK_LAUNCHED]
|
|
401
|
+
event_emissions:
|
|
402
|
+
- signal_name: PARENT_COMPLETE
|
|
403
|
+
|
|
404
|
+
background_workflow:
|
|
405
|
+
LongRunningTask:
|
|
406
|
+
node_type: tool
|
|
407
|
+
event_triggers: [START]
|
|
408
|
+
tool_name: long_task
|
|
409
|
+
context_parameter_field: task_data
|
|
410
|
+
output_field: result
|
|
411
|
+
event_emissions:
|
|
412
|
+
- signal_name: BACKGROUND_DONE
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
**Use cases:**
|
|
416
|
+
- Background processing
|
|
417
|
+
- Logging/analytics
|
|
418
|
+
- Notifications that don't affect the main flow
|
|
419
|
+
|
|
420
|
+
### Callback (Wait for Child)
|
|
421
|
+
|
|
422
|
+
Wait for specific signals from the child before continuing:
|
|
423
|
+
|
|
424
|
+
```yaml
|
|
425
|
+
# From child node configuration
|
|
426
|
+
signals_to_parent: [CHILD_DONE] # Wait for this signal
|
|
427
|
+
context_updates_to_parent: [result] # Get this data back
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
**Use cases:**
|
|
431
|
+
- Sequential processing
|
|
432
|
+
- When parent needs child's result
|
|
433
|
+
- Validation before proceeding
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## External Triggers and Continuation
|
|
438
|
+
|
|
439
|
+
Workflows don't have to complete in one `orchestrate()` call. When there are no more signals to process, the execution simply **stops**. It can be resumed at any time by sending new signals:
|
|
440
|
+
|
|
441
|
+
### The Pattern
|
|
442
|
+
|
|
443
|
+
```yaml
|
|
444
|
+
waiting_workflow:
|
|
445
|
+
Initialize:
|
|
446
|
+
node_type: router
|
|
447
|
+
event_triggers: [START]
|
|
448
|
+
event_emissions:
|
|
449
|
+
- signal_name: WAITING_FOR_APPROVAL
|
|
450
|
+
|
|
451
|
+
# This workflow pauses here. An external system must send APPROVED or REJECTED
|
|
452
|
+
# using broadcast_signals with the execution_id
|
|
453
|
+
|
|
454
|
+
HandleApproval:
|
|
455
|
+
node_type: router
|
|
456
|
+
event_triggers: [APPROVED]
|
|
457
|
+
event_emissions:
|
|
458
|
+
- signal_name: PROCESS_APPROVED
|
|
459
|
+
|
|
460
|
+
HandleRejection:
|
|
461
|
+
node_type: router
|
|
462
|
+
event_triggers: [REJECTED]
|
|
463
|
+
event_emissions:
|
|
464
|
+
- signal_name: PROCESS_REJECTED
|
|
465
|
+
|
|
466
|
+
ProcessApproved:
|
|
467
|
+
node_type: tool
|
|
468
|
+
event_triggers: [PROCESS_APPROVED]
|
|
469
|
+
tool_name: finalize_approved
|
|
470
|
+
output_field: final_result
|
|
471
|
+
event_emissions:
|
|
472
|
+
- signal_name: COMPLETE
|
|
473
|
+
|
|
474
|
+
NotifyRejection:
|
|
475
|
+
node_type: tool
|
|
476
|
+
event_triggers: [PROCESS_REJECTED]
|
|
477
|
+
tool_name: notify_rejection
|
|
478
|
+
output_field: rejection_notice
|
|
479
|
+
event_emissions:
|
|
480
|
+
- signal_name: COMPLETE
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### How It Works
|
|
484
|
+
|
|
485
|
+
1. Workflow starts, processes available signals, then stops (no more matching triggers)
|
|
486
|
+
2. Returns `execution_id` to the caller
|
|
487
|
+
3. Later: external system (human, API, event) sends a signal using that ID
|
|
488
|
+
4. Workflow resumes and continues processing
|
|
489
|
+
|
|
490
|
+
```python
|
|
491
|
+
# Start the workflow
|
|
492
|
+
execution_id = orchestrate(
|
|
493
|
+
config=waiting_workflow,
|
|
494
|
+
initial_workflow_name="waiting_workflow",
|
|
495
|
+
initial_signals=["START"],
|
|
496
|
+
...
|
|
497
|
+
)
|
|
498
|
+
# Execution stops after emitting WAITING_FOR_APPROVAL (no nodes listen for it yet)
|
|
499
|
+
|
|
500
|
+
# Later... external system sends approval
|
|
501
|
+
broadcast_signals(
|
|
502
|
+
execution_id=execution_id,
|
|
503
|
+
signals=["APPROVED"],
|
|
504
|
+
backends=backends,
|
|
505
|
+
)
|
|
506
|
+
# Workflow resumes and runs to completion
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
**Key insight**: There's no "waiting state" or "paused" status. The execution is simply stopped. Anyone with the `execution_id` can send a signal to trigger it again—even the original workflow you created months ago.
|
|
510
|
+
|
|
511
|
+
### Use Cases
|
|
512
|
+
|
|
513
|
+
- Human-in-the-loop approval
|
|
514
|
+
- External API callbacks (webhooks)
|
|
515
|
+
- Long-running processes that span days/weeks
|
|
516
|
+
- Event-driven architectures
|
|
517
|
+
- Triggering old executions with new data
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
521
|
+
## Versioning Strategies
|
|
522
|
+
|
|
523
|
+
SOE doesn't mandate a versioning strategy—it gives you the **primitives** to implement any strategy:
|
|
524
|
+
|
|
525
|
+
### Strategy 1: Context-Based Routing
|
|
526
|
+
|
|
527
|
+
Route to different workflow versions based on context:
|
|
528
|
+
|
|
529
|
+
```yaml
|
|
530
|
+
entry_workflow:
|
|
531
|
+
RouteByVersion:
|
|
532
|
+
node_type: router
|
|
533
|
+
event_triggers: [START]
|
|
534
|
+
event_emissions:
|
|
535
|
+
- signal_name: USE_V1
|
|
536
|
+
condition: "{{ context.api_version == 'v1' }}"
|
|
537
|
+
- signal_name: USE_V2
|
|
538
|
+
condition: "{{ context.api_version == 'v2' }}"
|
|
539
|
+
- signal_name: USE_LATEST
|
|
540
|
+
condition: "{{ context.api_version is not defined }}"
|
|
541
|
+
|
|
542
|
+
ExecuteV1:
|
|
543
|
+
node_type: child
|
|
544
|
+
event_triggers: [USE_V1]
|
|
545
|
+
child_workflow_name: processor_v1
|
|
546
|
+
child_initial_signals: [START]
|
|
547
|
+
input_fields: [request]
|
|
548
|
+
signals_to_parent: [V1_DONE]
|
|
549
|
+
context_updates_to_parent: [response]
|
|
550
|
+
|
|
551
|
+
ExecuteV2:
|
|
552
|
+
node_type: child
|
|
553
|
+
event_triggers: [USE_V2, USE_LATEST]
|
|
554
|
+
child_workflow_name: processor_v2
|
|
555
|
+
child_initial_signals: [START]
|
|
556
|
+
input_fields: [request]
|
|
557
|
+
signals_to_parent: [V2_DONE]
|
|
558
|
+
context_updates_to_parent: [response]
|
|
559
|
+
|
|
560
|
+
HandleV1Done:
|
|
561
|
+
node_type: router
|
|
562
|
+
event_triggers: [V1_DONE]
|
|
563
|
+
event_emissions:
|
|
564
|
+
- signal_name: COMPLETE
|
|
565
|
+
|
|
566
|
+
HandleV2Done:
|
|
567
|
+
node_type: router
|
|
568
|
+
event_triggers: [V2_DONE]
|
|
569
|
+
event_emissions:
|
|
570
|
+
- signal_name: COMPLETE
|
|
571
|
+
|
|
572
|
+
processor_v1:
|
|
573
|
+
ProcessOldWay:
|
|
574
|
+
node_type: llm
|
|
575
|
+
event_triggers: [START]
|
|
576
|
+
prompt: "Process (v1 legacy format): {{ context.request }}"
|
|
577
|
+
output_field: response
|
|
578
|
+
event_emissions:
|
|
579
|
+
- signal_name: V1_DONE
|
|
580
|
+
|
|
581
|
+
processor_v2:
|
|
582
|
+
ProcessNewWay:
|
|
583
|
+
node_type: llm
|
|
584
|
+
event_triggers: [START]
|
|
585
|
+
prompt: "Process with enhanced capabilities: {{ context.request }}"
|
|
586
|
+
output_field: response
|
|
587
|
+
event_emissions:
|
|
588
|
+
- signal_name: V2_DONE
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### Strategy 2: Natural Versioning (No Migration)
|
|
592
|
+
|
|
593
|
+
Since execution state includes the workflow definition:
|
|
594
|
+
|
|
595
|
+
1. **New version**: Just deploy new workflow YAML
|
|
596
|
+
2. **Existing executions**: Continue with their captured version
|
|
597
|
+
3. **New executions**: Use the new version
|
|
598
|
+
|
|
599
|
+
No migration needed—versions coexist naturally.
|
|
600
|
+
|
|
601
|
+
### Strategy 3: Context Migration
|
|
602
|
+
|
|
603
|
+
For executions that need to switch to a new workflow version:
|
|
604
|
+
|
|
605
|
+
1. **Read old context**: Get the execution's current state
|
|
606
|
+
2. **Transform context**: Map old fields to new schema
|
|
607
|
+
3. **Start new execution**: With the new workflow and migrated context
|
|
608
|
+
4. **Mark old execution**: Complete or archive it
|
|
609
|
+
|
|
610
|
+
```python
|
|
611
|
+
# Migrate an execution to a new workflow version
|
|
612
|
+
old_context = backends.context.get_context(old_execution_id)
|
|
613
|
+
|
|
614
|
+
migrated_context = migrate_context_v1_to_v2(old_context)
|
|
615
|
+
|
|
616
|
+
new_execution_id = orchestrate(
|
|
617
|
+
config=new_workflow_v2,
|
|
618
|
+
initial_workflow_name="main_workflow",
|
|
619
|
+
initial_signals=["RESUME"], # Custom signal for migrations
|
|
620
|
+
initial_context=migrated_context,
|
|
621
|
+
backends=backends,
|
|
622
|
+
broadcast_signals_caller=broadcast,
|
|
623
|
+
)
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
---
|
|
627
|
+
|
|
628
|
+
## Execution IDs: The Key to Everything
|
|
629
|
+
|
|
630
|
+
Every orchestration returns an `execution_id`. This ID is:
|
|
631
|
+
|
|
632
|
+
- **Unique**: Identifies this specific execution
|
|
633
|
+
- **Persistent**: Stored in the context backend
|
|
634
|
+
- **The key**: Used to send signals, read context, continue execution
|
|
635
|
+
|
|
636
|
+
### Sending Signals to Existing Executions
|
|
637
|
+
|
|
638
|
+
```python
|
|
639
|
+
# External system sends a signal
|
|
640
|
+
broadcast_signals(
|
|
641
|
+
execution_id="abc-123-def",
|
|
642
|
+
signals=["USER_APPROVED"],
|
|
643
|
+
backends=backends,
|
|
644
|
+
)
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### Reading Execution State
|
|
648
|
+
|
|
649
|
+
```python
|
|
650
|
+
# Inspect an execution
|
|
651
|
+
context = backends.context.get_context("abc-123-def")
|
|
652
|
+
print(context["current_step"])
|
|
653
|
+
print(context["__operational__"]["active_signals"])
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### Cross-Execution Communication
|
|
657
|
+
|
|
658
|
+
One execution can trigger signals in another:
|
|
659
|
+
|
|
660
|
+
```python
|
|
661
|
+
# In a tool function
|
|
662
|
+
def notify_other_workflow(other_execution_id: str) -> dict:
|
|
663
|
+
broadcast_signals(
|
|
664
|
+
execution_id=other_execution_id,
|
|
665
|
+
signals=["EXTERNAL_EVENT"],
|
|
666
|
+
backends=global_backends,
|
|
667
|
+
)
|
|
668
|
+
return {"notified": True}
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
---
|
|
672
|
+
|
|
673
|
+
## Key Takeaways
|
|
674
|
+
|
|
675
|
+
1. **Workflows are data** — Store, version, and compose them freely
|
|
676
|
+
2. **Executions are immutable** — Each run captures its workflow definition in the workflow backend
|
|
677
|
+
3. **Multiple workflows compose** — Build ecosystems, not monoliths
|
|
678
|
+
4. **Parallel is natural** — Fan-out to children for concurrent processing
|
|
679
|
+
5. **Executions stop, not pause** — Send signals anytime to continue any execution
|
|
680
|
+
6. **Execution IDs connect everything** — The key to cross-workflow communication
|
|
681
|
+
|
|
682
|
+
This ecosystem approach means you can:
|
|
683
|
+
- Deploy updates without breaking running processes
|
|
684
|
+
- Build sophisticated multi-workflow systems
|
|
685
|
+
- Handle long-running, event-driven processes
|
|
686
|
+
- Scale from simple scripts to enterprise orchestration
|
|
687
|
+
|
|
688
|
+
## Next Steps
|
|
689
|
+
|
|
690
|
+
Now that you understand the workflows ecosystem, let's explore [Infrastructure](guide_10_infrastructure.md) for custom backends and production deployments →
|