Download this notebook (.ipynb)

Agents: Build a Deterministic Workflow#

This advanced notebook constructs the orchestration that the simple pattern tutorial deliberately avoided. Two typed LogicStep handlers form a DAG: one scales scores and the dependent step summarizes them. The example stays offline so dependency handling, execution order, and result contracts remain visible.

Setup#

python -m pip install design-research-agents==0.6.0

Step 1: Define two deterministic handlers#

[1]:
from collections.abc import Sequence

import design_research_agents as agents


def scale_scores(context: agents.WorkflowContext) -> dict[str, object]:
    """Scale the input scores in the first workflow step."""
    scores = context.input_value("scores")
    scale = context.input_value("scale")
    if not isinstance(scores, Sequence) or isinstance(scores, str | bytes):
        raise ValueError("scores must be a sequence")
    if not all(isinstance(score, int | float) for score in scores):
        raise ValueError("scores must contain only numbers")
    if not isinstance(scale, int | float) or isinstance(scale, bool):
        raise ValueError("scale must be numeric")
    return {"scaled_scores": [float(score) * float(scale) for score in scores]}


def summarize_scores(context: agents.WorkflowContext) -> dict[str, object]:
    """Summarize the scaled scores produced by the dependency step."""
    scaled_scores = context.dependency_output("scale_scores").get("scaled_scores")
    if not isinstance(scaled_scores, Sequence) or isinstance(scaled_scores, str | bytes):
        raise ValueError("Scaled scores are missing")
    if not all(isinstance(score, int | float) for score in scaled_scores):
        raise ValueError("Scaled scores must contain only numbers")
    return {
        "count": len(scaled_scores),
        "mean": sum(float(score) for score in scaled_scores) / len(scaled_scores),
    }


print("Handlers:", scale_scores.__name__, "->", summarize_scores.__name__)
Handlers: scale_scores -> summarize_scores

Step 2: Assemble an explicit dependency graph#

[2]:
workflow = agents.Workflow(
    input_schema={
        "type": "object",
        "required": ["scores", "scale"],
    },
    steps=(
        agents.LogicStep(step_id="scale_scores", handler=scale_scores),
        agents.LogicStep(
            step_id="summarize_scores",
            dependencies=("scale_scores",),
            handler=summarize_scores,
        ),
    ),
)
print(workflow.to_mermaid(direction="LR"))
flowchart LR
    workflow_entry["Workflow Entrypoint"]
    step_1["scale_scores<br/>LogicStep"]
    step_2["summarize_scores<br/>LogicStep"]
    workflow_entry --> step_1
    step_1 --> step_2

Step 3: Run the DAG#

[3]:
result = workflow.run(
    {"scores": [0.2, 0.5, 0.8], "scale": 10},
    execution_mode="dag",
    request_id="tutorial-agents-workflow",
)
print("Workflow success:", result.success)
print("Execution order:", " -> ".join(result.execution_order))
Workflow success: True
Execution order: scale_scores -> summarize_scores

Step 4: Inspect intermediate and final outputs#

[4]:
scaled = result.step_results["scale_scores"].output_list("scaled_scores")
summary = result.step_results["summarize_scores"]
print("Scaled scores:", scaled)
print("Count:", summary.output_value("count"))
print("Mean:", f"{float(summary.output_value('mean', 0.0)):.1f}")
Scaled scores: [2.0, 5.0, 8.0]
Count: 3
Mean: 5.0

Next steps#

Substitute ToolStep, ModelStep, or DelegateStep only where external behavior is actually needed. Keeping preprocessing and validation in deterministic logic steps makes model-facing work smaller and easier to inspect.