Design: Workflow Graph Engineering

Orchestration nodes, contracts, fan-out, and verification.

Thesis#

A linear agent queues steps in one head. A workflow graph makes the shape of the work explicit: nodes do thinking; edges carry results; coordination is code, not another chat turn. That is what lets a fleet run without drowning the session context.

Primitives#

Nodes are jobs#

  • One agent, one bounded job, one input in, one output out.
  • Prefer schema-validated structured output at the tool/parse layer so the next node can consume without guessing.
  • A node that only produces free text is a node only a human can read — not wireable into a graph.

Edges are data contracts#

  • An edge means: this node’s output feeds that node’s input. Nothing more.
  • Name edges by data shape, not order, so you can swap endpoints if the shape holds.
  • Much “combine the results” work is flatten/dedupe — do it in plain code (zero tokens). A graph where every edge is an agent pays rent on its own wiring.

The fake-edge test#

For every “and then” in your agent:

Does the next step read the last step’s output?

If not, there is no edge — the wait is wasted. Cut it and run those nodes concurrently.

Degenerate graph → real graph#

A script A then B then C then D is already a graph: a single unbranching chain. It runs correctly and fails brittlely. First skill: redraw. Remove arrows that carry no data; independent nodes fan out into a merge that truly needs all of them.

Latency becomes the depth of real dependencies, not the sum of all steps.

Core topologies#

Fan-out with a barrier (parallel)#

  • N independent thunks → N concurrent subagents → array of results.
  • Barrier: waits for all before returning (next stage sees the complete set).
  • Failed thunk → null (or equivalent), not whole-batch reject. Always filter nulls.
  • Orchestration lives in code; each subagent has its own context — the parent never holds N sources at once.

Fan-in#

  • Use a barrier only when a stage genuinely needs every prior result together (cross-set dedupe, early-exit on empty total, compare-all prompt).
  • Smell: parallel → transform → parallel where the middle transform has no cross-item dependency → you should have used a streaming pipeline and skipped the barrier.

Diamond (default serious shape)#

Fan out → reduce → synthesize

  1. Fan out for breadth
  2. Reduce with plain code (compress, dedupe, filter)
  3. Synthesize with a final judgment agent

Once you see diamonds, you stop asking “how do I add more steps?” and ask “where’s the split, where’s the merge?”

Conditional routing#

  • Router node inspects validated output; if/switch in code picks the path.
  • Judgment can be model-powered; routing reliability comes from code. No emergent “Claude skipped the audit” unless that skip is written into the graph.

Verifiers on the edge#

Structure produces confidence:

PatternIdea
Adversarial verifyN skeptics try to refute; keep only if majority survive
Perspective-diverse verifyDistinct lenses (correctness, security, repro)
Judge panelN attempts, parallel judges, synthesize from winner + graft runners-up

Verifier context should be fresh — it has not seen the worker’s chat, only the finding.

Failure isolation#

  • Contain failures at the node (nulls in fan-out; fan-in tolerates missing inputs).
  • Parallel writers: isolate with git worktrees (or equivalent sandboxes). Only when nodes actually write in parallel — not a default tax.
  • Layered fan-in: never pour 1,000 raw outputs into one merge; chunk → summarize batches → final answer.

Cycles that converge#

For unknown-size discovery (bug sweeps):

  • Loop-until-dry: stop after K consecutive empty rounds.
  • Dedupe against everything seen, not only confirmed results — otherwise rejected findings reappear forever.

Model tiering#

  • Boring/repetitive fan-out nodes → cheaper model.
  • Merge / adjudication → expensive model.
  • Check session defaults before a large run; override per node.

Topology = cost and latency#

  • Prefer streaming pipelines when stages have no cross-item sync need.
  • Barriers are measurable wall-clock; “cleaner code” is not a reason for a barrier.

Self-routing / saved workflows#

  • Dynamic workflows: describe the objective; orchestrator writes the script and spawns the fleet.
  • Save good runs as named, editable workflow definitions.
  • Example skeleton: scope → parallel search → fetch → adversarial verify → synthesize.

Shipped operators: kind: plan | expand | goal on the declarative skill-DAG engine. A planner emits a validated WorkPlan; expand materializes subgoals as nested bodies (or child runs when isolation: run); goal loops plan→expand under max_plan_rounds / max_subgoals_per_wave, accumulates goal_state artifacts, and synthesizes when status is done. Coordination stays in code — models only emit WorkPlan JSON.

Shipped promote lift: POST /v1/runs/{id}/promote closes the emergent→explicit loop for capability episodes. Successful agent-loop calls[] become authored composio_toolkit_call nodes with pinned_tool_slugs (search skipped on replay). The capability template stays the Reuse surface; the library holds the lifted artifact.

Shipped freeze-DAG prompt tuning: hold WorkflowDef.dag fixed; score Prompt SkillDef.impl_ref variants against a generic eval corpus (verdict_complete / verifier_passed / json_path_equals / autophagy_score). Winners apply as ReviseSkillPrompt improvement proposals — structure and content stay separable (prompt graph G2/G4).

When not to use a graph#

  • Work is not wide / independent — a loop or single agent is enough.
  • You lack anchors (tests, real outcomes) — topology will amplify confident wrongness.
  • Budget/monitoring cannot absorb a fleet — start capped, measure, widen only after a run earns it.

Anchors (non-negotiable)#

Topology alone does not buy truth. Require nodes that cannot be argued with (tests that actually passed, external ground truth). Freeze rules an optimizer would weaken. A graph that grades its own reports fails later, more expensively, with greener lights.

Paste-ready patterns (design inventory)#

Use these as product templates for doso workflows:

  1. Decision-grade research desk (angles → parallel research → skeptic → ranked report)
  2. Security / auth sweep per route file + verify
  3. Unknown-size discovery loop with caps
  4. Adversarial review routed on risk/diff size
  5. Scheduled ecosystem / digest scan
  6. Launch kit with human gate before publish

Doso implementation#

Workflow graphs are the execution layer over personal data: ingest jobs, resolution jobs, verification jobs, and query/synthesis jobs should be designed as nodes with contracts, not as a single chat thread. The architecture shows how those jobs compose with the knowledge graph.

The persisted skill-DAG runtime implements these semantics in crates/doso-core/src/skills/engine.rs: dependency-ready scheduling, all/any/quorum joins, runtime JSON Schema validation, bounded retries and timeouts, deterministic routes, dynamic maps, convergent loops, nested subgraphs, fresh-context verifier voting, expected predecessor-count checks, caching, safe-result memoization, resource locks, and approval gates. Nested work shares one concurrency semaphore and aggregate node/invocation/depth budgets.

Isolation is effect-driven rather than universal. Code skills require the opt-in OS-sandboxed subprocess and temporary working directory; unsupported hosts fail closed. This is not a git worktree. External writes cannot be isolated by a local worktree, so connection execution keeps its short-lived exact-input approval digest while the DAG scheduler serializes shared resource keys across concurrent runs. Consequential effects are not cached. Pure and read-only nodes pay no isolation tax.

These are deliberate security boundaries. Local code runs in a fail-closed OS sandbox; external mutations use exact-input approval and shared resource locks.