{"record":{"id":"7150c06f8a354683","repo":"tinyhumansai/openhuman","slug":"workflow-scheduler-graph-run-failed-e","errorCode":null,"errorMessage":"workflow scheduler graph run failed: {e}","messagePattern":"workflow scheduler graph run failed: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/openhuman/agent/orchestration/workflow_runs/graph.rs","lineNumber":251,"sourceCode":"                    &session,\n                    &cancel,\n                    model_override,\n                    &phase,\n                    total_spawned,\n                )\n                .await\n            }\n        }\n    };\n\n    let graph = build_scheduler_graph(definition.phases.len(), select, run)?.with_event_sink(\n        Arc::new(GraphTracingSink::new(format!(\"workflow:{run_id_owned}\"))),\n    );\n\n    graph\n        .run(SchedulerState::default())\n        .await\n        .map_err(|e| anyhow!(\"workflow scheduler graph run failed: {e}\"))?;\n    Ok(())\n}\n\n/// Structure-only [`GraphTopology`] of the workflow scheduler graph for debug /\n/// inspection (issue #4249, Phase 4). Built with no-op stub effects — the\n/// topology exposes only node names, edges, and routing, never closure bodies.\npub(crate) fn scheduler_graph_topology() -> Result<GraphTopology> {\n    let graph = build_scheduler_graph(\n        1,\n        || async { Ok(PhaseSelection::Terminated) },\n        |_phase: WorkflowPhase, _spawned: u32| async { Ok(PhaseExecOutcome::Terminated) },\n    )?;\n    Ok(graph.topology())\n}\n","sourceCodeStart":233,"sourceCodeEnd":266,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/a221052e0df5b1f7598fceba7329fd1af95d6699/src/openhuman/agent/orchestration/workflow_runs/graph.rs#L233-L266","documentation":"Runtime failure of the compiled scheduler graph: graph.run(SchedulerState::default()) returned Err (graph.rs:251). Node bodies lift engine errors through graph_err — select_next_phase/execute_phase failures (ledger writes, spawn errors) become TinyAgentsError::Graph — and the RecursionPolicy backstop (max_visits_per_node = phase_count + 2, max_total_steps = (phase_count + 1) * 3 + 16) aborts a definition that never drains. A phase that merely fails is NOT this error: it persists Failed and routes to done.","triggerScenarios":"select_next_phase or execute_phase returning Err (run-ledger sqlite write failure, session error, spawn-budget enforcement erroring); a malformed WorkflowDefinition where dispatch keeps re-selecting phases until a node exceeds phase_count + 2 visits or the run exceeds (phase_count + 1) * 3 + 16 steps; run_phase reached with no selected phase.","commonSituations":"Hand-authored or LLM-generated workflow definitions with a dependency cycle that still 'progresses'; disk/lock problems on the run ledger; cancellation racing a phase transition; duplicate phase ids re-selecting the same phase forever.","solutions":["Read the GraphTracingSink event stream tagged workflow:{run_id} to find the exact node and step where the run aborted","Validate the WorkflowDefinition before starting: unique phase ids, acyclic dependencies, every phase completable — a valid DAG always drains within the policy budget","If the underlying cause is a ledger/session store error, fix that first (disk space, sqlite lock, workspace permissions)","If the recursion policy tripped, recount visits: dispatch fires once per phase plus one terminal no-phase visit; anything more means the definition loops"],"exampleFix":"// before — self-dependency keeps dispatch busy until the backstop fires\n{ \"id\": \"build\", \"depends_on\": [\"build\"] }\n\n// after\n{ \"id\": \"build\", \"depends_on\": [\"plan\"] }","handlingStrategy":"try-catch","validationCode":"// Caller-side guard before starting a run: reject definitions that cannot drain\nfn definition_is_dag(def: &serde_json::Value) -> bool {\n    let Some(phases) = def.get(\"phases\").and_then(|p| p.as_array()) else { return false };\n    let ids: std::collections::HashSet<&str> = phases\n        .iter().filter_map(|p| p.get(\"id\").and_then(|v| v.as_str())).collect();\n    ids.len() == phases.len() // unique ids; also keep depends_on acyclic\n}","typeGuard":null,"tryCatchPattern":"// Map scheduler failure onto the durable run status instead of propagating raw\nif let Err(e) = drive_phases(cfg, def, run_id, cancel, session).await {\n    tracing::error!(run_id, \"workflow run failed: {e:#}\");\n    persist_run_failed(run_id, &e).await; // run shows Failed in status + UI\n}","preventionTips":["Validate phase ids are unique and dependencies acyclic before start_workflow_run","Remember the policy budget: dispatch may fire only phase_count + 1 times — design definitions that provably drain","Watch the workflow:{run_id} tracing sink on first runs of a new definition shape"],"tags":["rust","workflow","scheduler","recursion","runtime"],"backgroundTag":null,"analyzedSha":"a221052e0df5b1f7598fceba7329fd1af95d6699","analyzedAt":"2026-08-16T12:47:06.542Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}