Quality review pipeline

Run two labelers concurrently, validate every stage, and adjudicate disagreements.

Use a Workflow to run two labelers in parallel, review their outputs, and call an adjudicator when the reviewer finds a disagreement. This runnable adaptation of the quality-review cookbook checks every agent result before consuming it. An unsuccessful or unparseable stage fails the workflow instead of treating missing review fields as agreement.

Save the following program as quality_pipeline.py:

quality_pipeline.py
from typing import List, Optional, TypeVar

from agno.agent import Agent
from agno.db.sqlite import SqliteDb
from agno.run.base import RunStatus
from agno.workflow import Step, Workflow
from agno.workflow.condition import Condition
from agno.workflow.parallel import Parallel
from agno.workflow.types import HumanReview, StepInput, StepOutput
from pydantic import BaseModel, Field


class Contact(BaseModel):
    name: Optional[str] = None
    email: Optional[str] = None
    phone: Optional[str] = None
    company: Optional[str] = None
    title: Optional[str] = None


class FieldDisagreement(BaseModel):
    field: str = Field(..., description="Top-level Contact field name")
    value_a: Optional[str] = None
    value_b: Optional[str] = None
    reason: str = Field(..., description="Why this field needs adjudication")


class DisagreementReport(BaseModel):
    disagreements: List[FieldDisagreement] = Field(default_factory=list)
    needs_adjudication: bool = Field(..., description="True if any field disagrees")


class FinalLabel(BaseModel):
    contact: Contact
    notes: Optional[str] = None

LABELER_INSTRUCTIONS = """\
Extract contact information from the input. Use exactly what the text
shows. If a field is missing, leave it null. Do not guess.
"""

labeler_a = Agent(
    name="Labeler A",
    model="google:gemini-3.5-flash",
    instructions=LABELER_INSTRUCTIONS,
    output_schema=Contact,
)

labeler_b = Agent(
    name="Labeler B",
    model="anthropic:claude-opus-4-7",
    instructions=LABELER_INSTRUCTIONS,
    output_schema=Contact,
)

reviewer = Agent(
    name="Reviewer",
    model="anthropic:claude-opus-4-7",
    instructions="""\
You are given two labelers' Contact outputs. Compare them field by field.
A field needs adjudication when both labelers report non-null but
different values. Emit one FieldDisagreement per such field. Set
needs_adjudication=true if any field needs adjudication.
""",
    output_schema=DisagreementReport,
)

adjudicator = Agent(
    name="Adjudicator",
    model="anthropic:claude-opus-4-7",
    instructions="""\
Re-read the original input text and resolve every reported disagreement.
Return a FinalLabel.contact populated with the correct values for all
fields (use the agreed values for fields not in dispute).
""",
    output_schema=FinalLabel,
)

Schema = TypeVar("Schema", bound=BaseModel)


def checked_run(agent: Agent, prompt: str, schema: type[Schema]) -> Schema:
    run = agent.run(prompt)
    if run.status != RunStatus.completed or not isinstance(run.content, schema):
        raise RuntimeError(f"{agent.name} did not return a validated {schema.__name__}")
    return run.content


def checked_step(step_input: StepInput, name: str, schema: type[Schema]) -> Schema:
    output = step_input.get_step_output(name)
    if output is None or not output.success or not isinstance(output.content, schema):
        raise RuntimeError(f"{name} has no successful {schema.__name__} output")
    return output.content


def run_labeler_a(step_input: StepInput) -> StepOutput:
    return StepOutput(content=checked_run(labeler_a, str(step_input.input), Contact))


def run_labeler_b(step_input: StepInput) -> StepOutput:
    return StepOutput(content=checked_run(labeler_b, str(step_input.input), Contact))


def run_reviewer(step_input: StepInput) -> StepOutput:
    a = checked_step(step_input, "Labeler A", Contact)
    b = checked_step(step_input, "Labeler B", Contact)
    prompt = (
        f"Labeler A:\n{a.model_dump_json(indent=2)}\n\n"
        f"Labeler B:\n{b.model_dump_json(indent=2)}"
    )
    return StepOutput(content=checked_run(reviewer, prompt, DisagreementReport))


def has_disagreement(step_input: StepInput) -> bool:
    report = checked_step(step_input, "Reviewer", DisagreementReport)
    return report.needs_adjudication


def run_adjudicator(step_input: StepInput) -> StepOutput:
    a = checked_step(step_input, "Labeler A", Contact)
    b = checked_step(step_input, "Labeler B", Contact)
    report = checked_step(step_input, "Reviewer", DisagreementReport)
    prompt = (
        f"Original input:\n{step_input.input}\n\n"
        f"Labeler A:\n{a.model_dump_json(indent=2)}\n\n"
        f"Labeler B:\n{b.model_dump_json(indent=2)}\n\n"
        f"Reviewer report:\n{report.model_dump_json(indent=2)}"
    )
    return StepOutput(content=checked_run(adjudicator, prompt, FinalLabel))


workflow = Workflow(
    name="Quality review labeling",
    db=SqliteDb(db_file="tmp/labeling.db"),
    steps=[
        Parallel(
            Step(name="Labeler A", executor=run_labeler_a, human_review=HumanReview(on_error="fail")),
            Step(name="Labeler B", executor=run_labeler_b, human_review=HumanReview(on_error="fail")),
            name="Label",
        ),
        Step(name="Reviewer", executor=run_reviewer, human_review=HumanReview(on_error="fail")),
        Condition(
            name="Adjudicate",
            evaluator=has_disagreement,
            human_review=HumanReview(on_error="fail"),
            steps=[
                Step(
                    name="Adjudicator",
                    executor=run_adjudicator,
                    human_review=HumanReview(on_error="fail"),
                ),
            ],
        ),
    ],
)

if __name__ == "__main__":
    result = workflow.run("Liam Ortega is a Support Engineer at Meadow. Email: liam@meadow.io.")
    if result.status != RunStatus.completed:
        raise RuntimeError("Labeling failed; send the original input to review")
    print(result.content)

Execution flow

StepWhat it does
ParallelRuns the Google and Anthropic labelers concurrently, checking each Contact.
ReviewerReads both named outputs with StepInput.get_step_output() and checks the returned DisagreementReport.
ConditionReads needs_adjudication from the checked reviewer output.
AdjudicatorReceives the original input, both labels, and the report; checks its FinalLabel.
SqliteDbPersists workflow runs in tmp/labeling.db.

The explicit HumanReview(on_error="fail") settings stop execution when an executor raises. They do not add a human approval pause. The agreement path leaves a skipped-Condition message as the workflow's final content. Read the Reviewer step for its typed DisagreementReport and the two labeler steps for their Contact outputs. The adjudication path returns a FinalLabel. Consumers should inspect the result type and step outputs rather than assume every completed run ends with the same schema.

The reviewer is instructed to flag a field when both labelers return non-null, different values. Change its instructions if a null value and a populated value should also trigger adjudication. Schema checks validate structure; test the model's comparison and extraction quality against labeled inputs separately.

Run the workflow

uv venv .venv --python 3.12
source .venv/bin/activate
uv pip install "agno[google,anthropic,sqlite]"
export GOOGLE_API_KEY="..."
export ANTHROPIC_API_KEY="..."
python quality_pipeline.py

Test against a labeled validation set before using the output. Track reviewer decisions and final labels by prompt and model version. The linked cookbook currently uses unchecked executor results; use the complete adaptation above when you need the explicit failure behavior shown here.

Next steps

TaskGuide
Define the label schemaData extraction
Inspect workflow patternsWorkflows
Run independent branchesParallel workflows
Gate a step on a resultConditional workflows

Developer Resources