You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs: update WORKFLOW_GUIDE with hooks, conditionals, parallel, nesting, retries, obligations
Complete reference for all 8 step types, 3 hooks, OPA obligations,
retry behavior, context chaining for parallel/nested workflows,
and a full pipeline example using every feature.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sub-steps in `then`/`else` can be any step type (`set`, `message`, `http`, `tool`). Context set by sub-steps is available to subsequent workflow steps.
304
+
305
+
Stored in context as `{{ step_id.branch }}` ("then" or "else") and `{{ step_id.condition }}`.
306
+
307
+
#### `parallel` — Concurrent execution
308
+
309
+
```yaml
310
+
- id: multi_search
311
+
action: parallel
312
+
steps:
313
+
- id: search_mp
314
+
action: http
315
+
method: GET
316
+
url: "https://api.materialsproject.org/materials?formula={{ formula }}"
317
+
- id: search_nomad
318
+
action: http
319
+
method: GET
320
+
url: "https://nomad-lab.eu/api/v1/entries?formula={{ formula }}"
321
+
- id: search_graph
322
+
action: http
323
+
method: GET
324
+
url: "{{ platform_api_base }}/knowledge/graph/search?q={{ formula }}"
325
+
```
326
+
327
+
All sub-steps execute concurrently via `tokio::spawn`. Each sub-step's context is merged back into the parent. In dry run, shows the plan without executing.
328
+
329
+
Stored in context as `{{ step_id.completed }}` (count) and `{{ step_id.steps }}` (list of sub-step IDs).
330
+
331
+
**Note:** Sub-steps run independently — they cannot reference each other's output. Use `parallel` for fan-out queries, not for dependent chains.
332
+
333
+
#### `workflow` — Call a sub-workflow
334
+
335
+
```yaml
336
+
- id: train_model
337
+
action: workflow
338
+
name: forge # Must exist in discovery paths
339
+
inputs:
340
+
paper: "{{ paper }}"
341
+
dataset: "{{ candidates }}"
342
+
target: "local"
343
+
```
344
+
345
+
Recursively executes another workflow with its own arguments, steps, and policy checks. The child workflow's full result (context + steps) is stored under the step ID.
**OPA policy:** The child workflow gets its own `workflow.execute` policy check. If the child is denied, the parent aborts.
350
+
351
+
#### Retries — on any step
352
+
353
+
Any step can have retry configuration:
354
+
355
+
```yaml
356
+
- id: flaky_api
357
+
action: http
358
+
method: GET
359
+
url: "https://unreliable-api.example.com/data"
360
+
retries: 3 # Retry up to 3 times on failure
361
+
retry_delay_secs: 2 # Base delay (multiplied by attempt number)
362
+
expect_status: [200]
363
+
```
364
+
365
+
Retry behavior:
366
+
- Attempt 0: immediate
367
+
- Attempt 1: wait `retry_delay_secs * 1` seconds
368
+
- Attempt 2: wait `retry_delay_secs * 2` seconds
369
+
- If all attempts fail, the workflow aborts with the last error
370
+
- Dry run mode never retries
371
+
372
+
Works on all step types: `set`, `message`, `http`, `tool`, `if`, `parallel`, `workflow`.
373
+
374
+
---
375
+
376
+
## Hooks
377
+
378
+
Hooks run before and after the main workflow steps. They're defined at the top level of the YAML:
379
+
380
+
```yaml
381
+
hooks:
382
+
on_start:
383
+
- id: notify_start
384
+
action: http
385
+
method: POST
386
+
url: "https://slack.example.com/webhook"
387
+
body:
388
+
text: "Workflow {{ workflow_name }} starting"
389
+
- id: log_start
390
+
action: message
391
+
text: "Starting {{ workflow_name }} at {{ now_iso }}"
392
+
393
+
on_complete:
394
+
- id: notify_done
395
+
action: http
396
+
method: POST
397
+
url: "https://slack.example.com/webhook"
398
+
body:
399
+
text: "Workflow {{ workflow_name }} completed"
400
+
401
+
on_error:
402
+
- id: notify_fail
403
+
action: message
404
+
text: "Workflow {{ workflow_name }} failed"
405
+
```
406
+
407
+
| Hook | When it runs | Context available |
408
+
|------|-------------|-------------------|
409
+
| `on_start` | Before any step, after argument resolution | Arguments + builtins only |
410
+
| `on_complete` | After all steps succeed | Full context including all step outputs |
411
+
| `on_error` | When any step fails (planned, not yet wired) | Context up to the failed step |
412
+
413
+
Hook steps can be `set`, `message`, or `http`. Hook failures are logged but don't abort the workflow.
414
+
415
+
---
416
+
417
+
## OPA Obligations
418
+
419
+
When OPA policy allows a workflow, it may also return **obligations** — things the system must do as a side effect.
420
+
421
+
### Built-in obligations
422
+
423
+
| Obligation | Trigger | Effect |
424
+
|------------|---------|--------|
425
+
| `audit_log` | Any `workflow.execute` action | Logs workflow start/complete via `tracing::info` with workflow name, principal, and timestamps |
426
+
| `notify_admin` | Agent role executing a workflow | Emits `tracing::warn` so admin dashboards/alerting can pick it up |
427
+
428
+
### Custom obligations
429
+
430
+
Add custom obligations in your `.rego` policy:
431
+
432
+
```rego
433
+
package prism.policy
434
+
435
+
# Require cost approval for expensive workflows
436
+
obligations contains "cost_approval" if {
437
+
input.action == "workflow.execute"
438
+
input.context.step_count > 10
439
+
}
440
+
441
+
# Require audit for any agent action
442
+
obligations contains "audit_log" if {
443
+
input.principal == "agent"
444
+
}
445
+
```
446
+
447
+
Obligations are returned in the `PolicyDecision.obligations` field and logged/acted on by the workflow engine. Custom obligation handlers can be added in the Rust workflow engine as needed.
└─ step 3 can read from step 1, step 2, and all args
649
+
└─ step 2 output added (if/parallel/workflow sub-steps also add)
650
+
└─ step 3 can read from step 1, step 2, all args, and hook context
651
+
on_complete hooks (can read full context)
472
652
```
473
653
654
+
For `parallel` steps, each sub-step runs with a snapshot of the current context. Sub-step outputs are merged back — if two sub-steps set the same key, last-to-finish wins.
655
+
656
+
For `workflow` (nesting), the child gets its own context built from `inputs`. The child's full result is stored under the parent step ID — access with `{{ step_id.context.variable }}`.
657
+
474
658
---
475
659
476
660
## Putting It Together: GFlowNet Exploration
@@ -498,3 +682,116 @@ The complete flow for adding a new GFlowNet exploration capability:
498
682
```
499
683
500
684
No Rust code changes. No CLI modifications. No compilation. The workflow engine handles discovery, argument parsing, template rendering, OPA policy, HTTP calls, tool dispatch, and result reporting.
685
+
686
+
---
687
+
688
+
## Complete Example: All Features
689
+
690
+
A workflow that uses every engine feature:
691
+
692
+
```yaml
693
+
api_version: prism/v1
694
+
kind: workflow
695
+
name: full-pipeline
696
+
command_name: pipeline
697
+
description: End-to-end materials discovery with all engine features.
698
+
699
+
arguments:
700
+
- name: formula
701
+
type: string
702
+
required: true
703
+
help: Chemical formula to investigate, e.g. NiCrCoAlTi
704
+
- name: target
705
+
type: string
706
+
default: yield_strength
707
+
help: Property to optimize
708
+
709
+
hooks:
710
+
on_start:
711
+
- id: h_start
712
+
action: message
713
+
text: "Pipeline starting for {{ formula }} targeting {{ target }}"
714
+
on_complete:
715
+
- id: h_done
716
+
action: http
717
+
method: POST
718
+
url: "https://hooks.example.com/notify"
719
+
body:
720
+
text: "Pipeline for {{ formula }} completed"
721
+
722
+
steps:
723
+
# 1. Search multiple databases in parallel
724
+
- id: search
725
+
action: parallel
726
+
steps:
727
+
- id: graph
728
+
action: http
729
+
method: GET
730
+
url: "https://api.marc27.com/api/v1/knowledge/graph/search?q={{ formula }}"
0 commit comments