7.2 KiB
Workflow Action workflow_file Field & Timeline Demo Workflow
Date: 2026-03-04 Scope: Pack loading architecture, workflow file discovery, demo workflow
Summary
Introduced a workflow_file field for action YAML definitions that separates action-level metadata from workflow graph definitions. This enables a clean conceptual divide: the action YAML controls ref, label, parameters, policies, and tags, while the workflow file contains the execution graph (tasks, transitions, variables). Multiple actions can reference the same workflow file with different configurations, which has implications for policies and parameter mapping.
Also created a comprehensive demo workflow in the python_example pack that exercises the Workflow Timeline DAG visualizer.
Architecture Change
Before
Workflows could be registered two ways, each with limitations:
workflows/directory (pack root) — scanned byWorkflowLoader, registered byWorkflowRegistrarwhich auto-creates a companion action. No separation of action metadata from workflow definition.- API endpoints (
POST /api/v1/packs/{ref}/workflow-files) — writes toactions/workflows/, creates bothworkflow_definitionand companionactionrecords. Only available via the visual builder, not during pack file loading.
The PackComponentLoader had no awareness of workflow files at all — it only loaded actions, triggers, runtimes, and sensors from their respective directories.
After
A third path is now supported, bridging both worlds:
- Action YAML with
workflow_filefield — an action YAML inactions/*.yamlcan includeworkflow_file: workflows/timeline_demo.yaml(path relative toactions/). During pack loading, thePackComponentLoader:- Reads and parses the referenced workflow YAML
- Creates/updates a
workflow_definitionrecord - Creates the action record with
workflow_defFK linked - Skips runtime resolution (workflow actions have no runner_type)
- Uses the workflow file path as the entrypoint
This preserves the clean separation the visual builder already uses (action metadata in one place, workflow graph in another) while making it work with the pack file loading pipeline.
Dual-Directory Workflow Scanning
The WorkflowLoader now scans two directories:
{pack_dir}/workflows/— legacy standalone workflow files{pack_dir}/actions/workflows/— visual-builder and action-linked workflow files
Files with .workflow.yaml suffix have the .workflow portion stripped when deriving the name/ref (e.g., deploy.workflow.yaml → name deploy, ref pack.deploy). If the same ref appears in both directories, actions/workflows/ wins. The reload_workflow method searches actions/workflows/ first with all extension variants.
Files Changed
Rust (crates/common/src/pack_registry/loader.rs)
- Added imports for
WorkflowDefinitionRepository,CreateWorkflowDefinitionInput,UpdateWorkflowDefinitionInput, andparse_workflow_yaml load_actions(): When action YAML containsworkflow_file, callsload_workflow_for_action()to create/update the workflow definition, sets entrypoint to the workflow file path, skips runtime resolution, and links the action to the workflow definition after creation/updateload_workflow_for_action()(new): Reads and parses the workflow YAML, creates or updates theworkflow_definitionrecord, respects action YAML schema overrides (action'sparameters/outputtake precedence over the workflow file's own schemas)
Rust (crates/common/src/workflow/loader.rs)
load_pack_workflows(): Now scans bothworkflows/andactions/workflows/, with the latter taking precedence on ref collisionreload_workflow(): Searchesactions/workflows/first, trying.workflow.yaml,.yaml,.workflow.yml, and.ymlextensions before falling back toworkflows/scan_workflow_files(): Strips.workflowsuffix from filenames (e.g.,deploy.workflow.yaml→ namedeploy)- 3 new tests:
test_scan_workflow_files_strips_workflow_suffix,test_load_pack_workflows_scans_both_directories,test_reload_workflow_finds_actions_workflows_dir
Python (scripts/load_core_pack.py)
upsert_workflow_definition()(new): Reads a workflow YAML file, upserts intoworkflow_definitiontable, returns the IDupsert_actions(): Detectsworkflow_filefield, callsupsert_workflow_definition(), sets entrypoint to workflow file path, skips runtime resolution for workflow actions, links action to workflow definition viaUPDATE action SET workflow_def = ...
Demo Pack Files (packs.external/python_example/)
actions/simulate_work.py+actions/simulate_work.yaml: New action that simulates a unit of work with configurable duration, optional failure simulation, and structured JSON outputactions/timeline_demo.yaml: Action YAML withworkflow_file: workflows/timeline_demo.yaml— controls action-level metadataactions/workflows/timeline_demo.yaml: Workflow definition with 11 tasks, 18 transition edges, exercising parallel fan-out/fan-in,with_items+ concurrency, failure paths, retries, timeouts, publish directives, and custom edge styling via__chart_meta__
Documentation
AGENTS.md: Updated Pack Component Loading Order, added Workflow Action YAML (workflow_filefield) section, added Workflow File Discovery (dual-directory scanning) section, added pitfall #7 (never put workflow content directly in action YAML), renumbered subsequent itemspacks.external/python_example/README.md: Added docs forsimulate_work,timeline_demoworkflow, and usage examples
Test Results
- 596 unit tests passing, 0 failures
- 0 compiler warnings across the workspace
- 3 new tests for the workflow loader changes, all passing
- Integration tests require
attune_testdatabase (pre-existing infrastructure issue, unrelated)
Timeline Demo Workflow Features
The python_example.timeline_demo workflow creates this execution shape:
initialize ─┬─► build_artifacts(6s) ────────────────┐
├─► run_linter(3s) ─────┐ ├─► merge_results ─► generate_items ─► process_items(×5, 3∥) ─► validate ─┬─► finalize_success
└─► security_scan(4s) ──┘ │ └─► handle_failure ─► finalize_failure
└───────────────┘
| Feature | How Exercised |
|---|---|
| Parallel fan-out | initialize → 3 branches with different durations |
| Fan-in / join | merge_results with join: 3 |
with_items + concurrency |
process_items expands to N items, concurrency: 3 |
| Failure paths | Every task has {{ failed() }} transitions |
| Timeout handling | security_scan has timeout: 30 + {{ timed_out() }} |
| Retries | build_artifacts and validate with retry configs |
| Publish directives | Results passed between stages |
| Custom edge colors/labels | Via __chart_meta__ on transitions |
| Configurable failure | fail_validation=true exercises the error path |