Skip to content

LLM output extends the running workflow: dynamic DAG growth and spawned child work

State: open/active · Genre: analysis

Use case: dynamic-workflow-growth RFC mapping: new (extends UC7) Source: cross-repo survey of sksizer/* (2026-07-05), companion to the workflow-constellation RFC (PR #682).

A recurring shape the static-DAG framing of UC7 misses: an LLM step’s output adds nodes to the workflow that is currently executing — a planning agent decomposes an epic into child tasks that must join the same traversal, an implementation run spawns one follow-up PR per friction finding, an orchestrator re-derives its ready set every tick because agents keep growing it. Today this is supported only by reload-and-repick hacks.

The planning workflow pins an opus agent to write child PRD files, and the graph executor explicitly supports ‘mid-run DAG growth’ by re-loading all PRDs between runs so agent-created children join the same invocation; PRD-551 specifies reload-between-batches for the parallel case.

Where: python/darkfactory/workflow/definitions/prd/planning/workflow.py · python/darkfactory/graph/_execution.py · .darkfactory/data/prds/PRD-551-parallel-graph-execution.md

Pain: The executor’s reload-and-repick loop exists largely to support agent-grown graphs — a hand-rolled workaround for a missing primitive.

task-work Step 8 fans out one /sdlc:spawn-task-pr invocation per classified friction bullet from the post-mortem (each landing as its own PR, potentially on foreign repos); the orchestrator re-derives ‘what is ready’ every tick because the task graph grows between ticks.

Where: plugin/skills/task-work/SKILL.md · plugin/skills/spawn-task-pr/SKILL.md · plugin/skills/orchestrate/SKILL.md

Pain: Child-work spawning is a prose-coordinated sub-agent dispatch with its own marker contract per spawn.

Next-work selection is re-derived by the LLM every iteration (‘pick the highest-priority story where passes:false’) from a ledger the previous iteration’s agent may have rewritten — the degenerate form of a dynamically re-evaluated work graph.

Where: CLAUDE.md · skills/ralph/SKILL.md

Pain: ‘What runs next’ is recomputed by an LLM per iteration because no scheduler owns a mutable graph.

Gap in @flow/dag as designed: it needs either first-class node append on a running graph (with re-planning of the ready frontier) or a child-workflow spawn primitive whose children join the parent’s scheduling, caps, and reporting. Without it, every planning-agent pattern falls back to reload hacks outside the library.

Builds on: 11–12 Introduces: extension: graph.append() on a running DAG

Nodes appended to a running graph from a step’s typed output — the primitive whose absence forces reload-and-repick hacks:

const g = dag("epic")
.task("plan", planAgent, { deps: [] }) // agent -> { children: ChildSpec[] }
.build();
await runDag(g, {
concurrency: 4,
onOutput: (step, out, graph) => {
if (step !== "plan") return;
for (const c of out.children) {
graph.append(c.id, taskFor(c), { deps: ["plan"] });
// appended nodes join THIS run's scheduling, caps, journal, and events
}
},
});

Entirely an extension: the RFC’s UC7 frames the DAG as static. The scheduler re-derives the ready frontier after each append — which is also exactly the incremental g.ready() form the queue-supervision evidence wants (11).


← Back to Notes