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¶
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¶
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