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 → parallelwhere 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
- Fan out for breadth
- Reduce with plain code (compress, dedupe, filter)
- 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/switchin 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:
| Pattern | Idea |
|---|---|
| Adversarial verify | N skeptics try to refute; keep only if majority survive |
| Perspective-diverse verify | Distinct lenses (correctness, security, repro) |
| Judge panel | N 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:
- Decision-grade research desk (angles → parallel research → skeptic → ranked report)
- Security / auth sweep per route file + verify
- Unknown-size discovery loop with caps
- Adversarial review routed on risk/diff size
- Scheduled ecosystem / digest scan
- 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.