4.1 KiB
Typed Publish Directives in Workflow Definitions
Date: 2026-03-04
Problem
The python_example.timeline_demo workflow action failed to execute with:
Runtime not found: No runtime found for action: python_example.timeline_demo
(available: node.js, python, shell)
This error was misleading — the real issue was that the workflow definition YAML
failed to parse during pack registration, so the workflow_definition record was
never created and the action's workflow_def FK remained NULL. Without a linked
workflow definition, the executor treated it as a regular action and dispatched it
to a worker, which couldn't find a runtime for a workflow action.
Root Cause
The YAML parsing error was:
tasks[7].next[0].publish: data did not match any variant of untagged enum
PublishDirective at line 234 column 11
The PublishDirective::Simple variant was defined as HashMap<String, String>,
but the workflow YAML contained non-string publish values:
publish:
- validation_passed: true # boolean, not a string
- validation_passed: false # boolean, not a string
YAML parses true/false as booleans, which couldn't deserialize into String.
Solution
Changed PublishDirective::Simple from HashMap<String, String> to
HashMap<String, serde_json::Value> so publish directives can carry any
JSON-compatible type: strings (including template expressions), booleans,
numbers, arrays, objects, and null.
Files Modified
| File | Change |
|---|---|
crates/common/src/workflow/parser.rs |
PublishDirective::Simple value type → serde_json::Value |
crates/executor/src/workflow/parser.rs |
Same change (executor's local copy) |
crates/executor/src/workflow/graph.rs |
Renamed PublishVar.expression: String → PublishVar.value: JsonValue with #[serde(alias = "expression")] for backward compat with stored task graphs; imported serde_json::Value |
crates/executor/src/scheduler.rs |
Updated publish map from HashMap<String, String> to HashMap<String, JsonValue> |
crates/executor/src/workflow/context.rs |
publish_from_result accepts HashMap<String, JsonValue>, passes values directly to render_json (strings get template-rendered, non-strings pass through unchanged) |
crates/common/src/workflow/expression_validator.rs |
Only validates string values as templates; non-string literals are skipped |
packs.external/python_example/actions/workflows/timeline_demo.yaml |
Fixed result().items → result().data.items (secondary bug in workflow definition) |
Type Preservation
The rendering pipeline now correctly preserves types end-to-end:
- String values (e.g.,
"{{ result().data }}") → rendered through expression engine with type preservation - Boolean values (e.g.,
true) → stored asJsonValue::Bool(true), pass throughrender_jsonunchanged - Numeric values (e.g.,
42,3.14) → stored asJsonValue::Number, pass through unchanged - Null → stored as
JsonValue::Null, passes through unchanged - Arrays/Objects → stored as-is, with any nested string templates rendered recursively
Tests Added
parser::tests::test_typed_publish_values_in_transitions— verifies YAML parsing of booleans, numbers, strings, templates, and null in publish directivesgraph::tests::test_typed_publish_values— verifies typed values survive graph constructioncontext::tests::test_publish_typed_values— verifies typed values pass throughpublish_from_resultwith correct types (boolean stays boolean, not string "true")
Verification
After deploying the fix:
- Re-registered
python_examplepack — workflow definition created successfully (ID: 2) - Action
python_example.timeline_demolinked toworkflow_def = 2 - Executed the workflow — executor correctly identified it as a workflow action and orchestrated 15 child task executions through all stages: initialize → parallel fan-out (build/lint/scan) → merge join → generate items → with_items(×3) → validate → finalize
- Workflow variables confirmed type preservation:
validation_passed: true(boolean),items_processed: 3(integer),number_list: [1, 2, 3](array)