feat(workflows): M3 — conditional when + input templating at run time
Wire the two pure M1 modules into the orchestrator's execute path:
- Before running a step, build a `RunContext` from the run input + every prior
succeeded step's output (`load_run_step_outputs`, read fresh at execution
time so a step sees all upstream results).
- `workflow_expr::eval` the step's `when` condition (if any): false → the step
is `skipped` — never executed — and counts as satisfied-but-empty for its
dependents (a new `StepOutcome::Skipped`, written in `complete_step_and_advance`
then advanced; `compute_advance` already treats skipped as satisfying deps).
- `workflow_template::resolve` the step's `input` against the context: an
exact single `{{ ref }}` preserves the referenced JSON type, an embedded ref
interpolates as text; a missing reference is a hard step failure (a definition
bug surfaced, not hidden), never a silent null.
`when` and templates were already parse-validated at apply time (M1); this is
the runtime half.
Tests (DB-gated, end-to-end via a scripted fake executor):
- `when_false_skips_step` — b skipped, never executed, omitted from run output
- `step_output_flows_into_downstream_input` — a's `{n:7}` feeds b's input,
type-preserved for a bare ref and interpolated in a larger string
- `missing_input_ref_fails_the_step` — an unresolved ref fails b → run fails
Verified: cargo fmt, clippy -D warnings clean, 448 manager-core lib tests,
11 workflow_orchestrator DB tests (3 new).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -330,6 +330,9 @@ pub enum StepOutcome {
|
||||
error: String,
|
||||
retry_delay: chrono::Duration,
|
||||
},
|
||||
/// The step's `when` condition evaluated false — it is `skipped` (never
|
||||
/// executed) and counts as satisfied-but-empty for its dependents (M3).
|
||||
Skipped,
|
||||
}
|
||||
|
||||
/// What `complete_step_and_advance` did — surfaced mostly for tests/logging.
|
||||
@@ -662,6 +665,22 @@ pub async fn complete_step_and_advance(
|
||||
return Ok(AdvanceResult::Stale);
|
||||
}
|
||||
}
|
||||
StepOutcome::Skipped => {
|
||||
let res = sqlx::query(
|
||||
"UPDATE workflow_run_steps \
|
||||
SET status = 'skipped', output = NULL, error = NULL, \
|
||||
claim_token = NULL, claimed_at = NULL, updated_at = NOW() \
|
||||
WHERE id = $1 AND claim_token = $2",
|
||||
)
|
||||
.bind(claimed.step_id.into_inner())
|
||||
.bind(claimed.claim_token)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
if res.rows_affected() == 0 {
|
||||
tx.rollback().await?;
|
||||
return Ok(AdvanceResult::Stale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Advance — lock the run row so concurrent advances of the same run
|
||||
@@ -884,6 +903,27 @@ pub async fn list_run_steps(
|
||||
rows.into_iter().map(TryInto::try_into).collect()
|
||||
}
|
||||
|
||||
/// The `{ step_name: output }` map of a run's **succeeded** steps — the
|
||||
/// accumulated context M3 resolves a step's `when` + `input` templates against
|
||||
/// (see `workflow_template::RunContext`). Read fresh at execution time so a
|
||||
/// step sees every prior step's output.
|
||||
pub async fn load_run_step_outputs(
|
||||
pool: &PgPool,
|
||||
run_id: WorkflowRunId,
|
||||
) -> Result<BTreeMap<String, serde_json::Value>, WorkflowRepoError> {
|
||||
let rows: Vec<(String, Option<serde_json::Value>)> = sqlx::query_as(
|
||||
"SELECT step_name, output FROM workflow_run_steps \
|
||||
WHERE run_id = $1 AND status = 'succeeded'",
|
||||
)
|
||||
.bind(run_id.into_inner())
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.filter_map(|(name, out)| out.map(|v| (name, v)))
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user