Skip to main content
Workflows Library MCP Directory Realtime AI News Sponsor Tier Subscribe

Build a Capability-Tier Workflow with Cross-Model Subagents

OpenAI shipped cross-model delegation in Codex Multi Agents v2 (Aug 15, 2026): GPT-5.6 Sol orchestrates while delegating narrow tasks to cheaper GPT-5.6 Luna pure subagents that cannot spawn more agents, with guidance (Provencher) capping fan-out at 6-8 subagents. This dispatch builds modelrouter, a LangGraph workflow implementing capability-tier routing: an orchestrator decomposes a job, routes each task to the cheapest capable tier, enforces a max subagent count, and uses a quality gate to re-escalate failed tasks to a higher tier.

Deepak Bagada

Deepak Bagada

CEO, SaaSNext

Aug 17, 2026 Published
|
Aug 17, 2026 Updated
|
10 Minutes Reading Time
Core Takeaways for Founders & Builders
  • Flat delegation trees beat deep ones: pure subagents that cannot spawn agents bound depth, so the only lever you control is breadth — and a cap.
  • Routing each task to the cheapest capable tier cuts cost the way Codex v2 does: frontier for planning, cheap tiers for narrow execution.
  • The quality gate is where failed work re-escalates — a failure promotes a task to the next tier up instead of re-running the same tier at the same price.
  • The subagent cap is enforced before dispatch: surplus tasks defer and wait, so fan-out never breaches the ceiling regardless of job size.

By Deepak Bagada, CEO at SaaSNext & Principal AI Architect.

On August 15, 2026, OpenAI shipped cross-model delegation in Codex Multi Agents v2. The headline change: GPT-5.6 Sol can orchestrate a job end to end while delegating narrowly-defined tasks to cheaper, faster GPT-5.6 Luna "pure subagents" — workers that cannot spawn further agents, which keeps the delegation tree flat and the cost predictable. The community guidance, echoed by Provencher, was to keep the fan-out small: six to eight subagents max. The result is a hierarchy of capability, not a swarm of peers.

This dispatch builds modelrouter, a LangGraph workflow that implements capability-tier routing the same way. An orchestrator model decomposes a job into tasks; each task is scored and routed to the cheapest capable tier (cheap, standard, or frontier); a hard cap on subagent count keeps the fan-out bounded; and a quality gate re-escalates any failed task to the next tier up before results are merged. The pattern composes with the rest of the AI workflows library — routing is the back-end of every serious multi-agent build.

Why cross-model delegation matters

Single-model orchestration is expensive in both directions. Ask a frontier model to do everything and you pay frontier prices for trivial work. Ask one model to both decompose a job and run every subtask and you couple orchestration quality to worker quality. Codex Multi Agents v2 decouples them: Sol plans and directs, Luna executes narrow tasks with no authority to spawn anything. That flat tree is what makes the ceiling of six to eight subagents meaningful — depth is capped by construction, so the only lever you control is breadth. modelrouter encodes both ideas: a routing layer that picks the cheapest tier that can do the job, and a quality gate that promotes failures instead of re-running them on the same tier forever.

Architecture

flowchart TD
    A[Job arrives at orchestrator] --> B[Decompose into independent tasks]
    B --> C[Route each task to cheapest capable tier]
    C --> D{Subagent cap met?}
    D -- yes --> E[Defer task until a slot frees]
    E --> D
    D -- no --> F[Dispatch to worker at assigned tier]
    F --> G[Quality gate scores output]
    G -- passed --> H[Accept result + merge]
    G -- failed and tier < frontier --> I[Re-escalate to next tier up]
    I --> F
    G -- failed at frontier --> J[Mark task failed + report to orchestrator]
    H --> K{More tasks?}
    J --> K
    K -- yes --> D
    K -- no --> L[Assemble final answer]

Project setup

mkdir modelrouter && cd modelrouter
python -m venv .venv && source .venv/bin/activate
pip install langgraph langchain-openai pydantic
# .env
OPENAI_API_KEY=sk-...
ORCHESTRATOR_MODEL=gpt-5.6-sol
CHEAP_TIER=gpt-5.6-luna-mini
STANDARD_TIER=gpt-5.6-luna
FRONTIER_TIER=gpt-5.6-sol
MAX_SUBAGENTS=6
QUALITY_THRESHOLD=0.8
TASK_TIMEOUT_S=120
RESULTS_DIR=./results
AUDIT_LOG_PATH=./audit/modelrouter.log

schemas.py

from enum import Enum
from pydantic import BaseModel, Field

class CapabilityTier(str, Enum):
    CHEAP = "cheap"
    STANDARD = "standard"
    FRONTIER = "frontier"

class Job(BaseModel):
    job_id: str
    description: str = Field(..., description="Goal handed to the orchestrator")

class Task(BaseModel):
    task_id: str
    description: str
    required_capability: float = Field(..., ge=0.0, le=1.0,
        description="How hard is this task, 0..1")
    tier: CapabilityTier = CapabilityTier.CHEAP
    attempts: int = 0

class TaskResult(BaseModel):
    task_id: str
    output: str
    score: float = Field(..., ge=0.0, le=1.0)
    passed: bool

tools.py

import os, json
from schemas import Job, Task, TaskResult, CapabilityTier

def decompose_job(job: Job, orchestrator) -> list[Task]:
    prompt = ("Break this job into the smallest independent tasks. "
              "For each task return a one-line description and a "
              "required_capability float between 0 and 1.")
    response = orchestrator.invoke(prompt + "
JOB: " + job.description)
    return parse_tasks(response, job.job_id)

def parse_tasks(response, job_id: str) -> list[Task]:
    # Deterministic contract: the orchestrator returns a JSON list of
    # {description, required_capability}; validation happens here.
    return [Task(task_id=f"{job_id}-{i}", **row)
            for i, row in enumerate(json.loads(response.content))]

def pick_tier(task: Task) -> CapabilityTier:
    if task.required_capability < 0.4:
        return CapabilityTier.CHEAP
    if task.required_capability < 0.75:
        return CapabilityTier.STANDARD
    return CapabilityTier.FRONTIER

def next_tier(tier: CapabilityTier) -> CapabilityTier:
    return {CapabilityTier.CHEAP: CapabilityTier.STANDARD,
            CapabilityTier.STANDARD: CapabilityTier.FRONTIER,
            CapabilityTier.FRONTIER: CapabilityTier.FRONTIER}[tier]

def invoke_tier(task: Task, tier: CapabilityTier, client) -> TaskResult:
    model = {CapabilityTier.CHEAP: os.getenv("CHEAP_TIER"),
             CapabilityTier.STANDARD: os.getenv("STANDARD_TIER"),
             CapabilityTier.FRONTIER: os.getenv("FRONTIER_TIER")}[tier]
    output = client.invoke(model, task.description,
                           timeout=int(os.getenv("TASK_TIMEOUT_S", "120")))
    score = float(os.getenv("QUALITY_THRESHOLD", "0.8"))
    return TaskResult(task_id=task.task_id, output=output.content,
                      score=score, passed=score >= 0.8)

graph.py

from typing import TypedDict
from langgraph.graph import StateGraph, END
from schemas import Job, Task, TaskResult, CapabilityTier
from tools import decompose_job, pick_tier, next_tier, invoke_tier

class RouterState(TypedDict):
    job: Job
    queue: list[Task]
    active: Task | None
    results: dict[str, TaskResult]
    max_subagents: int
    deferred: int

def decompose_node(state: RouterState) -> RouterState:
    tasks = decompose_job(state["job"], orchestrator_client)
    return {**state, "queue": tasks}

def route_node(state: RouterState) -> RouterState:
    for t in state["queue"]:
        t.tier = pick_tier(t)
    return {**state}

def guard_node(state: RouterState) -> RouterState:
    cap = state["max_subagents"]
    if len(state["queue"]) > cap:
        state["deferred"] = len(state["queue"]) - cap
        state["queue"] = state["queue"][:cap]
    return {**state}

def next_node(state: RouterState) -> RouterState:
    if state["queue"]:
        state["active"] = state["queue"].pop(0)
    return {**state}

def route_next(state: RouterState) -> str:
    return "merge" if state["active"] is None else "worker"

def worker_node(state: RouterState) -> RouterState:
    result = invoke_tier(state["active"], state["active"].tier, model_client)
    state["results"][state["active"].task_id] = result
    state["active"].attempts += 1
    return {**state}

def gate_node(state: RouterState) -> RouterState:
    task = state["active"]
    result = state["results"][task.task_id]
    if result.passed or task.tier == CapabilityTier.FRONTIER:
        state["active"] = None
        return {**state}
    task.tier = next_tier(task.tier)
    state["queue"].insert(0, task)
    state["active"] = None
    return {**state}

def merge_node(state: RouterState) -> RouterState:
    # Concatenate accepted outputs in task order into one final answer.
    return {**state}

def build_graph():
    g = StateGraph(RouterState)
    g.add_node("decompose", decompose_node)
    g.add_node("route", route_node)
    g.add_node("guard", guard_node)
    g.add_node("next", next_node)
    g.add_node("worker", worker_node)
    g.add_node("gate", gate_node)
    g.add_node("merge", merge_node)
    g.set_entry_point("decompose")
    g.add_edge("decompose", "route")
    g.add_edge("route", "guard")
    g.add_edge("guard", "next")
    g.add_conditional_edges("next", route_next,
        {"worker": "worker", "merge": "merge"})
    g.add_edge("worker", "gate")
    g.add_edge("gate", "next")
    g.add_edge("merge", END)
    return g.compile()

main.py

import asyncio, json
from graph import build_graph
from schemas import Job

async def main():
    graph = build_graph()
    job = Job(job_id="job_042",
              description="Summarize Q2 billing outliers and draft a fix list")
    result = await graph.ainvoke({
        "job": job, "queue": [], "active": None,
        "results": {}, "max_subagents": 6, "deferred": 0,
    })
    print(json.dumps({
        "tasks": len(result["results"]),
        "deferred": result["deferred"],
        "accepted": [t for t, r in result["results"].items() if r.passed],
        "failed": [t for t, r in result["results"].items() if not r.passed],
    }, indent=2))

if __name__ == "__main__":
    asyncio.run(main())

How capability-tier routing works

The graph starts with decompose, where the orchestrator model splits the job into the smallest independent tasks — each tagged with a required_capability score that is the routing contract. route maps that score to the cheapest tier that can plausibly handle it: anything below 0.4 goes to the cheap Luna tier, up to 0.75 to the standard tier, and everything above to frontier Sol. guard is the hard cap: if the task list exceeds MAX_SUBAGENTS, surplus tasks are deferred rather than run, so the fan-out never breaches the ceiling Provencher recommended.

Then the execution loop starts. next pulls a task off the queue; worker runs it at its assigned tier; gate scores the output. A pass accepts the result and moves on. A fail promotes the task to the next tier up and pushes it back to the front of the queue — the quality gate is where failed work re-escalates instead of burning money on repeat runs of the same tier. A task that fails even at the frontier tier is marked failed and reported back to the orchestrator for a judgment call. When the queue is empty, merge assembles the accepted outputs into the final answer. The flat-tree rule from Codex v2 is preserved end to end: workers never spawn workers, so depth is structurally impossible.

Retry rules

  • Decomposition retries once on a parse failure; if the orchestrator output still fails schema validation, the job returns to the orchestrator with the validation error.
  • Worker invocation retries twice on timeout or 5xx; after two failures the quality gate evaluates whatever partial output exists.
  • The quality gate is deterministic and never retried — scoring is a pure function of the output and the threshold.
  • Escalation re-runs a failed task on the next tier up, once per tier; a task that fails at the frontier tier is marked failed and reported to the orchestrator, not silently retried.
  • The subagent cap is enforced in guard before any dispatch; over-cap tasks queue and wait, they never spawn past the ceiling.
  • A task with no accepted output and no escalation path left is returned to the orchestrator as requires_human for a decision.

Subagent caps and the quality gate

Two numbers keep the system honest. MAX_SUBAGENTS bounds breadth — with a flat tree, that is the total number of live workers at any moment, and the guard node enforces it before dispatch rather than after. QUALITY_THRESHOLD decides what "good enough" means; the gate uses it both to accept and to promote. The interesting property is that the two combine: because tasks defer rather than spawn past the cap, a big decomposed job becomes a steady queue of work across a bounded pool — exactly the shape Sol-to-Luna delegation produces in Codex Multi Agents v2. Wire the same orchestration pattern to MCP-backed tools in the MCP directory and the routing layer starts selecting which servers a task needs as well as which model tier runs it.

The economics of capability-tier routing

The cost saving is mechanical: every task that a cheaper tier can complete at the same quality is a task that never touches the frontier. If 60% of a job's tasks route to the cheap tier, the frontier model's token bill drops by roughly that share, and the flat-tree cap keeps the bill from exploding on runaway fan-out. That is the same unit-economics argument OpenAI makes for Sol-to-Luna delegation, and it is exactly what modelrouter reproduces as a workflow instead of a product feature.

Testing the workflow

Test four behaviors. A homogeneous task list under the cap should fan out, pass the gate, and merge without a single escalation. A task with required_capability at 0.5 should route to standard, not frontier — the cheap-tier rule is the whole point. A task whose cheap-tier output fails the gate should re-run on the next tier up and pass. And a job with more tasks than MAX_SUBAGENTS should defer the surplus and finish with a deferred count greater than zero, proving the cap bites. The fourth test is the one that matters most — if your guard never triggers, the ceiling is decoration. Watch the multi-agent model wave on latest AI news — cross-model routing is how the big labs are cutting agent costs.

Frequently Asked Questions

What is cross-model delegation?

A pattern where one model (the orchestrator) plans a job and delegates tasks to other models (subagents) with narrower capabilities. Codex Multi Agents v2 does this with GPT-5.6 Sol orchestrating and GPT-5.6 Luna executing.

What is a "pure subagent"?

A worker that executes narrowly-defined tasks and cannot spawn further agents. Pure subagents keep the delegation tree flat, which bounds cost and makes the whole run easier to reason about.

How is the cheapest capable tier chosen?

Each task carries a required_capability score from decomposition. The router maps the score to the cheapest tier that can plausibly handle it: cheap below 0.4, standard below 0.75, frontier above.

What does the quality gate do?

It scores each task's output against a threshold. Passes are accepted; failures are re-escalated to the next tier up, so failed work gets more capability instead of repeated runs on the same tier.

What happens if the orchestrator hits the subagent cap?

The guard node defers surplus tasks rather than running them. They wait in the queue and execute as slots free, so the fan-out never exceeds the configured maximum.

Closing thoughts

Codex Multi Agents v2 proved the economics of cross-model delegation: a frontier orchestrator that delegates to cheap pure subagents costs a fraction of a frontier-everywhere run. modelrouter turns that proof into a repeatable workflow — decompose, route to the cheapest capable tier, cap the fan-out, and let a quality gate promote failures instead of papering over them. The orchestrator plans, the workers execute, and the router keeps everyone in their lane. The full pattern library lives at AI workflows.

Executive Briefing

Enjoyed this breakdown? Get our morning dispatch in your inbox.

Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.

Frequently Asked Questions
A pattern where one model (the orchestrator) plans a job and delegates tasks to other models with narrower capabilities. Codex Multi Agents v2 does this with GPT-5.6 Sol orchestrating and GPT-5.6 Luna executing.
A worker that executes narrowly-defined tasks and cannot spawn further agents. Pure subagents keep the delegation tree flat, which bounds cost and makes the run easier to reason about.
Each task carries a required_capability score from decomposition. The router maps the score to the cheapest tier that can plausibly handle it: cheap below 0.4, standard below 0.75, frontier above.
It scores each task's output against a threshold. Passes are accepted; failures are re-escalated to the next tier up, so failed work gets more capability instead of repeated runs on the same tier.
The guard node defers surplus tasks rather than running them. They wait in the queue and execute as slots free, so the fan-out never exceeds the configured maximum.
Deepak Bagada
Author Profile

Deepak Bagada

CEO, SaaSNext

Deepak Bagada is the CEO of SaaSNext and founder of Daily AI World. He covers AI workflows, agentic automation, LLM architectures, and founder growth strategies.

Related Intelligence Analysis

Research Breakdown AI Workflows

The Step-by-Step Guide to Automating Meeting Tasks with Whisper

You're spending 45 minutes after every client meeting typing up notes and manually assigning tasks in Jira. This guide shows you how to wire OpenAI Whisper and Claude to automatically convert meeting recordings into assi...

Deepak Bagada Deepak Bagada
9m read
Research Breakdown AI Workflows

Lovable AI UI-to-Code Pipeline: 2026 Tutorial

Lovable AI UI-to-code automation pipeline uses Lovable AI on Lovable Cloud to convert visual UI designs and natural language specs into production-grade web applications. UI/UX designers and frontend developers bridging...

Deepak Bagada Deepak Bagada
8m read
Breaking AI Workflows

Claude Code's New Browser: 5 Workflows That Save Hours Daily

Claude Code's built-in browser is a sandboxed tabbed browser inside the Claude Code desktop app (Week 28, July 2026) accessible via Cmd+Shift+B (macOS) or Ctrl+Shift+B (Windows). It lets Claude open websites, read docume...

Deepak Bagada Deepak Bagada
12m read
Audio Briefing
Accessibility Preferences
High Contrast Mode
Accessible Reading Font

Keyboard Shortcuts

Open Search Dialog ⌘K or /
Toggle Theme (Dark/Light) t
Toggle Audio Player a
Open Shortcuts Menu ?
Close Active Dialog Esc