Skip to content

Workflows and the DAG

An experiment is a directed acyclic graph. workflow.json declares the nodes and the typed edges between them; the runner executes that graph in topological order; validate proves the graph is sound before anything runs.

The file

{
  "name": "Kepler's Third Law from exoplanet data",
  "description": "Query the archive, fit the power law, test the exponent.",
  "nodes": [ ... ],
  "edges": [ ... ]
}

Nodes

Field Default Meaning
id Unique within the DAG. Also names the agent file.
name the id Human-readable label.
kind "algorithm" One of the seven agent kinds.
purpose "" Why this node exists. Read by humans and coding agents.
ports empty {"inputs": [...], "outputs": [...]}.
module agents/<id>.py Where the implementation lives.
tools [] Tools this node intends to call — enforced by a gate.
params {} Static parameters, readable via self.param(...).
workflow "" For composite nodes: the nested workflow file.
{
  "id": "analyze",
  "name": "Fit the power law",
  "kind": "algorithm",
  "purpose": "Fit T = C × a^α in log-log space to recover the Kepler exponent.",
  "ports": {
    "inputs":  [{ "name": "planets", "type": "json" }],
    "outputs": [{ "name": "fit_result", "type": "json", "description": "Exponent, r², diagnostics" }]
  },
  "tools": ["power_law_fit"]
}

tools is a promise the runtime checks

Listing a tool commits the node to actually calling one. If the node runs successfully without a single successful tool call, the tool_use_verified gate fails and the run scores 0.00. This is what catches a hard-coded answer.

Edges

An edge names both endpoints and both ports:

{ "source": "analyze", "source_port": "fit_result",
  "target": "evaluate", "target_port": "fit_result" }

Port names on the two sides do not have to match, but their types must be compatible.

Execution

science-adk run executes nodes strictly sequentially in a deterministic topological order computed from the edges. Sibling nodes do not run in parallel; async exists so a node can await I/O, not so the DAG can interleave. Determinism is worth more than wall-clock speed when the output is a scientific claim.

Before creating a run directory, the runner validates the workflow and refuses to start if it is unsound:

error: Workflow is not valid, so nothing was run:
error  edge fetch.planets -> analyze.planet: 'analyze' has no input port 'planet'
       fix: Available inputs: planets

Selective execution

science-adk run --only analyze evaluate   # these nodes and their dependents
science-adk run --reuse                   # reuse unchanged nodes from the last run
science-adk run --reuse 20260820-053012   # reuse from a specific run

--reuse compares each node's fingerprint — a hash of its source code, its resolved inputs and its params. Unchanged nodes are marked cached and their recorded outputs are passed downstream; anything whose fingerprint moved is re-executed, along with everything downstream of it. Cached nodes still count as having run for the all_nodes_ran gate, because their outputs came from a real earlier execution of identical code.

What validate proves

science-adk validate

Everything below is checked statically, in milliseconds, with no execution.

Graph structure

Check Level
The workflow has at least one node error
Node ids are unique error
Every edge endpoint names a known node error
The named source output port and target input port both exist error
Edge types are compatible error
An input port is fed by at most one edge error
The graph is acyclic error
Every required input port is connected error
No node is orphaned in a multi-node graph warning
At least one node has kind evaluation warning

Agent source

Each node's Python file is parsed and checked against its declaration:

Check Level
The file exists and parses error
It defines exactly one Agent subclass error
The class does not define run error
It defines execute, and execute is async error
ports is annotated (ports: Ports = Ports(...)) error
Other class attributes are annotated (ClassVar[...]) error
The class's port names match workflow.json exactly error

Integrity

The AST is also read for patterns that turn a failure into a fake result:

Pattern Level Why
except ...: pass — an exception caught and discarded error "Let it propagate. A swallowed error becomes a fabricated result."
bare except: warning Catches bugs along with the error you meant to handle.
eval() / exec() warning Executes arbitrary strings. Compute the value instead.
warnings.filterwarnings(...) warning Numerical warnings usually indicate a real problem.

Errors block the run. Warnings do not, but each one is worth a sentence of justification in the audit rationale.

Viewing the graph

science-adk graph

prints Mermaid, which renders directly in GitHub, in this site, and in most editors:

graph LR
    config["config<br/><i>config</i>"] -->|settings| fetch["fetch<br/><i>data</i>"]
    config -->|settings| evaluate["evaluate<br/><i>evaluation</i>"]
    fetch -->|planets| analyze["analyze<br/><i>algorithm</i>"]
    analyze -->|fit_result| evaluate

Reference