Run-Pass Options: Plays That Are Two Plays

How one snap carries a run and a pass, why the quarterback picks after the play has started, and why NFL RPOs look nothing like college ones

Show the Python (shared helpers for this chapter’s diagrams)
import os
import shutil
import logging

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, Ellipse, FancyArrowPatch, Rectangle, RegularPolygon
from matplotlib.lines import Line2D
from gridiron import Play, Player, Field, routes as r, style

logging.getLogger("matplotlib.font_manager").setLevel(logging.ERROR)
GREEN = "#0f7a55"            # a darker step of the aqua highlight, for text and branches on the light field
RED = style.DEFENSE_DARK
OL_NAMES = ("LT", "LG", "C", "RG", "RT")
LINE_D, OFF_BALL, GUN = -0.7, -1.5, -5.0


def pdf_build():
    return os.environ.get("RTG_FORMAT", "").lower() in ("pdf", "typst", "print")


# ---------------------------------------------------------------- players and alignments
def O(key, pos, d, w, label=None):
    return Player(key, pos, "off", d, w, label=label or key)


def D(key, pos, d, w, label):
    return Player(key, pos, "def", d, w, label=label)


def line5():
    """The five offensive linemen, 1.6 yards apart, each labelled so the reader can follow him."""
    return [O(k, k, LINE_D, w, k) for k, w in zip(OL_NAMES, (-3.2, -1.6, 0.0, 1.6, 3.2))]


def trips(rb_w=-1.6, x_w=-15.0, y_w=4.8, h_w=10.0, z_w=16.0):
    """11 personnel, shotgun, 3x1: X alone on the left (on the line), the tight end Y attached on the right
    (on the line), H in the slot and Z wide on the right, both a step off the ball. Seven on the line:
    the five linemen plus X and Y, so X and Y are the ends of the line and everyone else is a back."""
    return line5() + [O("QB", "QB", GUN, 0.0, "Q"), O("RB", "RB", GUN, rb_w, "R"),
                      O("X", "WR", LINE_D, x_w), O("Y", "TE", LINE_D, y_w),
                      O("H", "WR", OFF_BALL, h_w), O("Z", "WR", OFF_BALL, z_w)]


def nickel(shell="two_high", **moves):
    """A 4-2-5 nickel against trips right. Defensive linemen are labelled by technique: a 5 on the left
    tackle's outside shoulder, a 3 on the left guard's outside shoulder, a 1 shading the center, and a 7
    on the tight end's inside shoulder. Mike and Will linebackers 4 1/2 yards deep; the nickel back ($)
    apexed between the tight end and H. shell="two_high": two safeties 12 yards deep.
    shell="single_high": the free safety in the deep middle and the strong safety walked down as the
    overhang defender on the single-receiver side. moves = {key: (d, w)} adjusts anyone."""
    out = [D("BE", "DE", 1.0, -4.0, "5"), D("BT", "DT", 1.0, -2.35, "3"),
           D("PT", "DT", 1.0, 0.7, "1"), D("PE", "DE", 1.0, 4.2, "7"),
           D("MIKE", "MIKE", 4.5, 1.8, "M"), D("WILL", "WILL", 4.5, -2.2, "W"),
           D("NB", "NB", 5.0, 7.4, "$"), D("LCB", "CB", 7.0, -15.8, "CB"), D("RCB", "CB", 7.0, 16.8, "CB")]
    if shell == "two_high":
        out += [D("SS", "SS", 12.0, 9.0, "SS"), D("FS", "FS", 12.5, -8.5, "FS")]
    else:
        out += [D("SS", "SS", 5.5, -9.0, "SS"), D("FS", "FS", 13.0, -0.5, "FS")]
    return [q.moved(d=moves[q.key][0], w=moves[q.key][1]) if q.key in moves else q for q in out]


def only(players, lo, hi, dmax=30.0):
    """Keep the players inside a lateral window (w from lo to hi) for a zoomed static diagram."""
    return [q for q in players if lo <= q.w <= hi and q.d <= dmax]


def check_legal(players):
    """Seven offensive players on the line, and only the two at its ends may be eligible receivers."""
    on = sorted([q for q in players if q.side == "off" and q.d > -1.0], key=lambda q: q.w)
    assert len(on) == 7, [q.key for q in on]
    for q in on[1:-1]:
        assert q.key in OL_NAMES, f"{q.key} is covered up but not a lineman"


check_legal(trips())


def go_to(p, key, end, kind="block", delay=0.0, speed=None, via=()):
    """Move `key` through absolute (d, w) points `via` to `end`, as a block, path, run or route."""
    start = np.array(p._cur(key))
    pts = [tuple(np.array(q) - start) for q in (*via, end)]
    if kind == "block":
        return p.block(key, pts=pts, delay=delay, speed=speed)
    if kind == "route":
        return p.route(key, pts, frame="field", delay=delay, speed=speed)
    return {"path": p.path, "run": p.run, "pull": p.pull}[kind](key, pts, delay=delay, speed=speed)


# ---------------------------------------------------------------- drawing helpers
def _P(fld, p, d, w):
    return fld.xy(*p.to_abs(d, w))


def clean(ax):
    """Zoomed diagrams: drop hash ticks and painted yard numbers; clip text to the window."""
    for ln in list(ax.lines):
        xs, ys = ln.get_data()
        if len(xs) == 2 and ln.get_linewidth() == 0.5 and np.hypot(xs[1] - xs[0], ys[1] - ys[0]) < 1.0:
            ln.remove()
    for t in list(ax.texts):
        if t.get_text().isdigit() and t.get_alpha() == 0.55:
            t.remove()
        else:
            t.set_clip_on(True)


def draw(p, width_in=6.0, ax=None, **kw):
    fld = p.draw(width_in=width_in, ax=ax, title=False, **kw)
    clean(fld.ax)
    return fld


def say(fld, p, d, w, text, color=style.INK_2, fontsize=7.2, ha="center", weight="normal", box=True):
    """A short on-field label at (d, w)."""
    x, y = _P(fld, p, d, w)
    fld.ax.text(float(x), float(y), text, fontsize=fontsize, color=color, ha=ha, va="center", zorder=13,
                fontweight=weight, linespacing=1.15,
                bbox=dict(boxstyle="round,pad=0.18", fc=style.SURFACE, ec="none", alpha=0.9) if box else None)


def point_to(fld, p, text_at, target, text, color=style.INK_2, fontsize=7, ha="center", shrink=7):
    """A label with a thin leader line to the player or spot it names."""
    (xt, yt), (xp, yp) = _P(fld, p, *text_at), _P(fld, p, *target)
    fld.ax.annotate(text, (float(xp), float(yp)), xytext=(float(xt), float(yt)), fontsize=fontsize,
                    color=color, ha=ha, va="center", zorder=13, linespacing=1.15,
                    arrowprops=dict(arrowstyle="-", color=color, lw=0.7, shrinkA=2, shrinkB=shrink),
                    bbox=dict(boxstyle="round,pad=0.18", fc=style.SURFACE, ec="none", alpha=0.9))


def branch(fld, p, pts, color=GREEN, lw=1.8, dash=(0, (3, 2.2)), end="arrow", z=6):
    """A dashed path through absolute (d, w) points: one branch of a read, or a defender's reaction."""
    xs, ys = _P(fld, p, np.array([q[0] for q in pts]), np.array([q[1] for q in pts]))
    fld.ax.plot(xs, ys, color=color, lw=lw, ls=dash, zorder=z, solid_capstyle="round")
    if end == "arrow":
        fld.ax.add_patch(FancyArrowPatch((xs[-2], ys[-2]), (xs[-1], ys[-1]), arrowstyle="-|>",
                                         mutation_scale=10, color=color, lw=lw, zorder=z))


def throw(fld, p, a, b, color=style.BALL, rad=-0.08):
    """The pass: a dashed brown arrow from (d, w) a to (d, w) b."""
    (x0, y0), (x1, y1) = _P(fld, p, *a), _P(fld, p, *b)
    fld.ax.add_patch(FancyArrowPatch((float(x0), float(y0)), (float(x1), float(y1)), arrowstyle="-|>",
                                     mutation_scale=10, color=color, lw=1.5, ls=(0, (2, 2)), zorder=7,
                                     connectionstyle=f"arc3,rad={rad}"))


def read_line(fld, p, a, b, color=style.THIRD):
    """The quarterback's eyes: a dashed green line between two (d, w) spots."""
    (x0, y0), (x1, y1) = _P(fld, p, *a), _P(fld, p, *b)
    fld.ax.plot([x0, x1], [y0, y1], color=color, lw=1.3, ls=(0, (4, 2)), zorder=4)


def tackle_x(fld, p, d, w, color=RED, ms=10):
    x, y = _P(fld, p, d, w)
    fld.ax.plot([float(x)], [float(y)], marker="X", ms=ms, color=color, mec="white", mew=0.8, zorder=12)


def region(fld, p, d0, d1, w0, w1, color, alpha=0.12, ls="-", lw=0.9, fill=True):
    """A rectangle in (d, w): the box, or a rule's legal zone."""
    (xa, ya), (xb, yb) = _P(fld, p, d0, w0), _P(fld, p, d1, w1)
    x, y = min(float(xa), float(xb)), min(float(ya), float(yb))
    fld.ax.add_patch(Rectangle((x, y), abs(float(xb) - float(xa)), abs(float(yb) - float(ya)),
                               fc=color if fill else "none", alpha=alpha if fill else 1.0, ec="none", zorder=1.5))
    fld.ax.add_patch(Rectangle((x, y), abs(float(xb) - float(xa)), abs(float(yb) - float(ya)),
                               fc="none", ec=color, lw=lw, ls=ls, zorder=1.6))


def panel_title(ax, text, fs=9):
    ax.set_title(text, fontsize=fs, loc="left", color=style.INK, fontweight="bold")


LEGEND = {
    "block": Line2D([], [], color=style.OFFENSE_DARK, lw=1.8, marker="|", ms=8, mew=2.2, label="block"),
    "route": Line2D([], [], color=style.OFFENSE, lw=1.8, label="route"),
    "give": Line2D([], [], color=style.OFFENSE_DARK, lw=2.0, ls=(0, (3, 2.2)), label="run branch"),
    "keep": Line2D([], [], color=GREEN, lw=2.0, ls=(0, (3, 2.2)), label="QB keep"),
    "pass": Line2D([], [], color=style.BALL, lw=1.4, ls=(0, (2, 2)), label="pass branch"),
    "read": Line2D([], [], color=style.THIRD, lw=1.3, ls=(0, (4, 2)), label="QB's eyes"),
    "react": Line2D([], [], color=RED, lw=1.6, ls=(0, (3, 2.2)), label="defender's choice"),
    "fake": Line2D([], [], color=style.OFFENSE, lw=1.6, label="fake / path"),
}


def legend(fig, keys, y=0.02, fs=7, ncol=None):
    hs = [LEGEND[k] for k in keys]
    fig.legend(handles=hs, loc="upper center", bbox_to_anchor=(0.5, y), ncol=ncol or len(hs), fontsize=fs,
               frameon=False, handlelength=2.2, columnspacing=1.2)


# ---------------------------------------------------------------- frame strips (print) and video (web)
MARK_R = 0.72


def tracking(play):
    df = play.to_tracking()
    df["_id"] = df["displayName"].astype(str)
    return df


def catch_time(play):
    """Seconds after the snap at which the first pass arrives."""
    for kind, to, te in play.ball_events:
        if kind == "pass":
            a, b = play.pass_target_spot()
            return te + float(np.hypot(*(b - a))) / 18.0
    return None


def carrier(play, t):
    """Who holds the ball at time t (None while a pass is in the air)."""
    holder = "QB"
    for kind, to, te in sorted(play.ball_events, key=lambda e: e[2]):
        if kind == "handoff" and t >= te:
            holder = to
        if kind == "pass" and t >= te:
            holder = to if t >= catch_time(play) else None
    return holder


def panel(ax, play, df, t, lateral, window, rings=()):
    """One moment of a simulated play: markers, faint trails, the ball (and its flight), rings."""
    tmap = df.groupby("frameId")["time"].first()
    fid = int((tmap - t).abs().idxmin())
    ylo, yhi = sorted((play.ball_y - lateral[0], play.ball_y - lateral[1]))
    fld = Field(ax=ax, orient="vertical", los=play.los, window=window, numbers=False, hashes=False,
                lateral=(ylo, yhi), to_go=play.to_go)
    cur = df[df["frameId"] == fid]
    shown = cur[cur["x"].between(play.los + window[0] + 0.4, play.los + window[1] - 0.4)
                & cur["y"].between(ylo + 0.4, yhi - 0.4)]
    ids = set(shown["_id"])
    trails = df[(df["frameId"] <= fid) & df["_id"].isin(ids) & (df["side"] != "ball")]
    who_now = carrier(play, t)
    for (pid, side), g in trails.groupby(["_id", "side"]):
        xs, ys = fld.xy(g["x"].values, g["y"].values)
        if pid == who_now and pid != "QB":       # the ball carrier's path, drawn so it reads in print
            ax.plot(xs, ys, color=style.OFFENSE_DARK, lw=1.8, alpha=0.9, zorder=3.5)
            continue
        ax.plot(xs, ys, color=style.OFFENSE if side == "off" else style.DEFENSE, lw=0.9, alpha=0.4, zorder=3)
    passes = [te for kind, _, te in play.ball_events if kind == "pass"]
    if passes and t > passes[0]:
        b = df[(df["side"] == "ball") & (df["frameId"] <= fid) & (df["time"] >= passes[0] - 0.05)]
        xs, ys = fld.xy(b["x"].values, b["y"].values)
        ax.plot(xs, ys, color=style.BALL, lw=1.3, ls=(0, (2, 1.5)), zorder=9)
    ppy = fld.pts_per_yard()
    for _, rr in shown.iterrows():
        x, y = (float(v) for v in fld.xy(rr["x"], rr["y"]))
        if rr["side"] == "ball":
            ax.add_patch(Ellipse((x, y + 0.45), 0.8, 0.5, fc=style.BALL, ec="white", lw=0.5, zorder=11))
            continue
        lab = str(rr.get("label", "") or "")
        if lab in OL_NAMES and lab != "C":
            lab = ""                       # too small to read in a strip; the center keeps his letter
        if rr["side"] == "off":
            ax.add_patch(Circle((x, y), MARK_R, fc=style.OFFENSE, ec="white", lw=0.6, zorder=8))
            tc = "white"
        else:
            ax.add_patch(RegularPolygon((x, y), 4, radius=MARK_R * 1.3, orientation=np.pi / 4,
                                        fc=style.DEFENSE_TINT, ec=style.DEFENSE, lw=1.0, zorder=8))
            tc = style.INK
        fs = max(4.2, min(7.5, ppy * (0.95 if len(lab) <= 1 else 0.68)))
        ax.text(x, y, lab, ha="center", va="center", fontsize=fs, color=tc, fontweight="bold", zorder=10)
    who = carrier(play, t)
    for key, col in list(rings) + ([(who, style.THIRD)] if who else []):
        rr = cur[cur["displayName"] == key]
        if len(rr):
            x, y = fld.xy(rr["x"].iloc[0], rr["y"].iloc[0])
            ax.add_patch(Circle((float(x), float(y)), 1.05, fc="none", ec=col, lw=1.7, zorder=7))
    return fld


def strip(plays, times, lateral, window, notes=None, rows=None, rings=(), width_in=6.5):
    """Frame strip: one row per branch of the read, the same moments in each row."""
    dfs = [tracking(p) for p in plays]
    cells = [(i, t) for i in range(len(plays)) for t in times]
    ncols = len(times)
    nrows = len(plays)
    pw = width_in / ncols
    span_w, span_d = abs(lateral[1] - lateral[0]), window[1] - window[0]
    fig, axes = plt.subplots(nrows, ncols, squeeze=False,
                             figsize=(width_in, nrows * (pw * span_d / span_w + (0.75 if notes else 0.3))))
    for k, (ax, (i, t)) in enumerate(zip(axes.flat, cells)):
        panel(ax, plays[i], dfs[i], t, lateral, window, rings)
        head = f"{k % ncols + 1}.  " + ("snap" if abs(t) < 1e-6 else f"{t:+.1f} s")
        if rows and k % ncols == 0:
            head = rows[i] + "   " + head
        ax.set_title(head, fontsize=8, color=style.INK, loc="left",
                     fontweight="bold" if rows and k % ncols == 0 else "normal")
        if notes:
            ax.text(0.5, -0.03, notes[k], transform=ax.transAxes, ha="center", va="top", fontsize=7.0,
                    color=style.INK, linespacing=1.2)
    fig.tight_layout(h_pad=1.6 if notes else 0.6, w_pad=0.5)
    return fig


def video(plays, times, lateral, window, notes=None, rows=None, rings=(), width_in=6.4):
    """Web version: the branches side by side, drawn every tenth of a second."""
    from matplotlib import animation
    from IPython.display import HTML, display
    dfs = [tracking(p) for p in plays]
    t0 = min(float(df["time"].min()) for df in dfs)
    t1 = max(float(df["time"].max()) for df in dfs)
    grid = np.round(np.arange(max(t0, -0.5), t1 + 1e-9, 0.1), 2)
    n = len(plays)
    span_w, span_d = abs(lateral[1] - lateral[0]), window[1] - window[0]
    pw = width_in / n
    fig, axes = plt.subplots(1, n, squeeze=False, figsize=(pw * n, pw * span_d / span_w + 0.7))
    axes = list(axes.flat)

    def update(t):
        for i, ax in enumerate(axes):
            ax.clear()
            panel(ax, plays[i], dfs[i], t, lateral, window, rings)
            txt = "snap" if abs(t) < 0.05 else f"{t:+.1f} s"
            if notes:
                mine = notes[i * len(times):(i + 1) * len(times)]
                cur = [m for tt, m in zip(times, mine) if tt <= t + 1e-6]
                txt += ("\n" + cur[-1].replace("\n", " ")) if cur else ""
            ax.set_title((rows[i] + ": " if rows else "") + txt, fontsize=7.5, color=style.INK, loc="left")
        return []

    anim = animation.FuncAnimation(fig, update, frames=list(grid), interval=100)
    html = (anim.to_html5_video(embed_limit=40).replace("<video ", '<video style="max-width:100%;height:auto" ')
            if shutil.which("ffmpeg") else anim.to_jshtml(default_mode="once"))
    plt.close(fig)
    display(HTML(html))


def rpo_animation(plays, times, lateral, window, notes=None, rows=None, rings=(), **kw):
    """Video on the website; a numbered, captioned frame strip in the PDF."""
    if pdf_build():
        strip(plays, times, lateral, window, notes=notes, rows=rows, rings=rings, **kw)
        plt.show()
    else:
        video(plays, times, lateral, window, notes=notes, rows=rows, rings=rings)

First-and-10 on the opponent’s 40, early in the second quarter. The offense is in the shotgun with three receivers to the right and one alone on the left. At the snap the whole offensive line lurches to the right, run blocking, and the quarterback sticks the ball into the running back’s stomach. Everything about it says inside run, and the linebacker on the left side believes it: he takes two quick steps forward to fill his gap. Then the quarterback pulls the ball back out, turns, and throws to the receiver on the left, who has run upfield and bent inside, and is standing ten yards deep in the space behind where that linebacker was a second ago. Twelve yards, first down. The broadcast analyst says “RPO” the way people say “algorithm,” as if the word explained itself. So: was that a run or a pass? It was both, on purpose. The offense called a run and a pass on the same snap and let one defender’s two steps decide which one it would run. This chapter is about plays that are two plays: what they are, why they work, why the NFL’s version looks different from college’s, and how to see the decision coming.

You’ll be able to…

NoteYou’ll need
  • Option Football: the option principle (leave one defender unblocked on purpose and read him), the read key, the mesh and its ride, give / keep, the zone read, and the scrape exchange.
  • Play-Action, Bootlegs, and Screens: play-action is a pass play dressed as a run: the back fakes, the line mostly pass-blocks, and the throw was always the plan.
  • A First Look at Defense: the box and the box count, light and loaded boxes, hat math, the apex alignment and the overhang defender, man and zone coverage, one-high and two-high shells.
  • Zone Running: inside zone, combo blocks, and linemen climbing to the linebackers at the second level.
  • The Quick Game: the slant, stick, bubble and now screens, and receiver leverage. Passing Fundamentals: the conflict defender.
  • The Twenty-Two: who is an eligible receiver (only the players at the ends of the line and the backs) and who is not (the five interior linemen).

How to read the diagrams. Offense is blue circles, defense orange squares; left and right mean the offense’s left and right. Q is the quarterback and R the running back; X is the receiver alone on the left, Y the tight end, H the slot receiver and Z the wide receiver on the right. LT, LG, C, RG and RT are the offensive linemen. Defensive linemen are labelled by technique number (03-01): a 5 sits on the tackle’s outside shoulder, a 3 on the guard’s, a 1 shades the center, a 7 sits on the tight end’s inside shoulder (numbering systems vary; many staffs call that last alignment a 6i). M and W are the Mike and Will linebackers, $ is the nickel back, CB a cornerback, SS and FS the safeties. Short bars end blocks, thin arrows are routes and paths, thick dark arrows are ball carriers, and the yellow line across the field, when there is one, is the line to gain. On the read diagrams, the dashed green line from the quarterback is his eyes, a dashed dark-blue arrow is the run branch, a dashed brown arrow is the pass branch, and a dashed red arrow is a defender’s possible choice. An orange ring marks the defender being read; in the animations a green ring follows the ball.

One snap, two plays

A run-pass option, or RPO, is a play on which the offensive line blocks a run while one or more receivers run pass routes, and the quarterback decides which of the two to use. He decides either before the snap, by counting defenders, or just after it, by watching one defender while he holds the ball in the running back’s belly. If the defense has set itself to stop the run, he throws. If it has set itself to stop the pass, he hands off.

That sounds like every play: of course you run against a pass defense and throw against a run defense. The difference is when and by whom the choice is made. On an ordinary play the coordinator chooses, from the sideline, some forty seconds before the snap, on a guess about what the defense will do. On an RPO the coordinator calls both plays and hands the choice to the one person on the field who can see what the defense actually did, at the last possible moment. Coaches call a call like this a packaged play: one call that bundles a run with one or more passes, plus rules for picking. Every RPO is a packaged play; the older, broader term also covers packages with a run, a quarterback keep and a pass, which you will meet near the end of the chapter.

An RPO sits between two plays you already know, and the easiest way to understand it is to put the three side by side.

Show the Python
off2 = line5() + [O("QB", "QB", GUN, 0.0, "Q"), O("RB", "RB", GUN, -1.6, "R"),
                  O("H", "WR", OFF_BALL, -9.5), O("Y", "TE", LINE_D, 4.8)]
front = [D("BE", "DE", 1.0, -4.0, "5"), D("BT", "DT", 1.0, -2.35, "3"), D("PT", "DT", 1.0, 0.7, "1"),
         D("PE", "DE", 1.0, 4.2, "7"), D("MIKE", "MIKE", 4.5, 1.8, "M"), D("WILL", "WILL", 4.5, -2.2, "W"),
         D("NB", "NB", 4.8, -6.4, "$")]
fig, axes = plt.subplots(1, 3, figsize=(6.8, 2.75))
LAT, WIN = (-12.5, 8.5), (-7.0, 11.8)
GIVE = [(-5.0, -1.6), (-4.6, -0.2), (-2.0, 1.6), (3.0, 2.9)]


def zone_line(p, read_end=True):
    """Inside zone to the offense's right, every lineman stepping right. On the zone read the left tackle
    steps inside and leaves the backside end alone; otherwise he blocks him."""
    if read_end:
        go_to(p, "LT", (0.35, -2.6))
    else:
        go_to(p, "LT", (0.35, -4.2))
    for k, end in [("LG", (0.35, -1.1) if read_end else (0.35, -2.5)), ("C", (0.35, 0.6)),
                   ("RG", (0.35, 2.0)), ("RT", (0.35, 3.8)), ("Y", (0.35, 4.6))]:
        go_to(p, k, end)


# play-action
p = Play(off2, front, los=35, to_go=None)
for k in OL_NAMES:
    p.block(k, pts=[(-1.1, 0.0)])
go_to(p, "RB", (-3.0, 2.4), "path", via=[(-4.6, -0.2)])
go_to(p, "MIKE", (2.7, 2.2), "path"); go_to(p, "WILL", (2.7, -1.8), "path")
go_to(p, "H", (9.4, -0.5), "route", via=[(8.0, -9.5)])
p.pass_to("H", t=2.3)
f = draw(p, ax=axes[0], lateral=LAT, window=WIN)
say(f, p, -3.4, 4.9, "line\npass-blocks", fontsize=6.4)
say(f, p, 5.8, 5.2, "LBs bite\non the fake", fontsize=6.3, color=RED)
say(f, p, -5.4, 3.2, "R fakes", fontsize=6.3, color=style.OFFENSE_DARK)
panel_title(axes[0], "Play-action: run is fake", 8.5)
# zone read
p = Play(off2, front, los=35, to_go=None)
zone_line(p)
p.highlight("BE", color=style.DEFENSE)
f = draw(p, ax=axes[1], lateral=LAT, window=WIN)
read_line(f, p, (-4.6, -0.3), (0.6, -3.7))
branch(f, p, GIVE, color=style.OFFENSE_DARK)
branch(f, p, [(-5.0, 0.0), (-4.4, -1.6), (-1.8, -5.0), (2.8, -6.0)], color=GREEN)
say(f, p, 3.0, 5.4, "give", color=style.OFFENSE_DARK, fontsize=6.6, weight="bold")
say(f, p, 1.6, -8.9, "keep", color=GREEN, fontsize=6.6, weight="bold")
panel_title(axes[1], "Option: two runs", 8.5)
# RPO
p = Play(off2, front, los=35, to_go=None)
zone_line(p, read_end=False)
go_to(p, "H", (3.8, -7.8), "route", via=[(1.0, -9.5)])
p.highlight("NB", color=style.DEFENSE)
f = draw(p, ax=axes[2], lateral=LAT, window=WIN)
read_line(f, p, (-4.6, -0.4), (4.3, -5.9))
branch(f, p, GIVE, color=style.OFFENSE_DARK)
throw(f, p, (-5.0, -0.3), (3.6, -7.8), rad=0.12)
branch(f, p, [(4.2, -6.6), (2.2, -6.1)], color=RED, lw=1.6)     # the $ stepping down to the run
say(f, p, 3.0, 5.4, "give", color=style.OFFENSE_DARK, fontsize=6.6, weight="bold")
say(f, p, 6.4, -9.4, "throw", color=style.BALL, fontsize=6.6, weight="bold")
panel_title(axes[2], "RPO: one run, one pass", 8.5)
fig.tight_layout(w_pad=0.4, rect=(0, 0.12, 1, 1))
legend(fig, ["block", "route", "give", "keep", "pass", "read", "react"], y=0.125, fs=6.4, ncol=4)
plt.show()
Figure 25.1: Same formation, same backfield action, three different plays. Left, play-action: the line pass-blocks (short bars a step behind the line), the back fakes a run without the ball, the linebackers step up to stop it, and the quarterback throws behind them to H; the pass was the plan from the start. Middle, the zone read (03-04): the line run-blocks to the right, nobody blocks the backside end (ringed), and the quarterback reads him to give or keep; both branches are runs. Right, an RPO: the line run-blocks to the right as in the middle (this time the left tackle blocks the end), but now the read is the nickel back ($, ringed), who must help against the run and cover H. The red arrow is his run choice: if he steps down to the run, the quarterback throws to H in the space he left; if he stays with H, the quarterback hands off.
Table 25.1: Three plays that start the same way.
Play-action Option (03-04) RPO
What’s real Only the pass; the run is a fake Both runs Both the run and the pass
Who chooses, and when The coordinator, before the snap The quarterback, from the read key’s first step, after the snap The quarterback, from a count before the snap or a read just after it
What the line does Pass-blocks, made to look like run blocking Run-blocks Run-blocks, but in the NFL may not go more than a yard downfield if a pass is coming
Who is “left alone” Nobody: the fake does the work A defender in the run fit (usually the end) A defender with a run job and a pass job
What the quarterback watches Coverage, after the fake The read key The conflict defender, or the count

Read the table’s middle column next to the right one. The option reads a defender whose only job is the run: the end can tackle the back or the quarterback, so the offense makes him pick one. The RPO reads a defender who has a run job and a pass job, and makes him pick one of those. That is why it is called an option at all. It is the same arithmetic as the zone read, moved out of the backfield and into the space between the box and the receivers.

And read the left column against the right. Play-action also wants a linebacker to step up, but it needs him to believe the fake, and if he doesn’t, the quarterback is holding the ball in a pocket with no plan B except a throw into coverage. The RPO doesn’t need anyone to be fooled. If the linebacker steps up, throw. If he doesn’t, the run is real, and the run is now going against a defender who is standing in the wrong place to stop it.

ImportantWhy it exists

RPOs exist because modern defenses ask a few players to do two jobs at once, and an offense that waits to see which job they choose can always take the other one. Spread formations pulled defenses out of the box, and defenses answered with hybrid players (the nickel back, the walked-down safety, the apexed linebacker) who stand halfway between the box and a slot receiver and promise to help with both. That promise is the target. The trade: an RPO commits the offense to a simple run and a simple throw (complicated plays don’t fit in half a second), its linemen must obey a rule that run blocking normally ignores, and the quarterback has to make a clean decision while turning his body away from half the field. Most wrong reads are not a disaster, which is the beauty of it: a handoff that should have been a throw is still a run into a blocked box. The one costly mistake is the opposite, a throw into a defender who only pretended to play the run, and that is why, as you’ll see, the run is the default.

NoteMadden vs. real life

In the game, RPOs live in their own corner of the playbook, and you, the player, decide by pressing a button. That much is real: the quarterback really does decide. What the game hides is how little time he has and how little he is looking at. On a real post-snap RPO the quarterback is not scanning the field. The play call tells him which kind of defender to read (“the backside linebacker,” “the man over the slot”); before the snap he finds who that is today, then watches only him for about two-thirds of a second at the mesh, and either lets go of the ball into the back’s belly or pulls it. Most of the time, he hands it off. An NFL RPO is mostly a run play with a safety valve, not a pass play that sometimes runs.

Nobody else on the offense knows which branch is coming, and nobody needs to. The receivers run their routes every time (on a handoff they are decoys, pulling defenders away from the run), and the linemen block the run every time. A receiver who blocks a cornerback on a bubble RPO that turns into a handoff is simply blocking on a running play, and blocking downfield on a run is allowed: the downfield limits you are about to meet apply only when a forward pass is thrown.

Who gets read: the conflict defender

Every RPO is built around the defender with two jobs, the conflict defender of Passing Fundamentals. On an RPO his two jobs are a run gap and a pass area.

The usual suspect is the defender you met in A First Look at Defense: the overhang defender, standing in an apex alignment halfway between the last man in the box and a slot receiver. On a run toward him, he has to come in and fill the edge of the box; on a pass, he has to widen and take the slot’s short route. The apex is a promise to do both a step late, and the RPO calls the bluff. In a nickel defense he is usually the nickel back; against a single receiver, he may be a safety walked down or an outside linebacker.

He is the most common target, not the only one. Any defender whose run job and pass job pull in opposite directions will do:

  • An inside linebacker. On a run, he steps forward to fill a gap; on a pass, he drops back into a zone (or turns to cover a back or tight end in man). Two steps forward for the run open the space behind him.
  • A safety who fits the run. Against two-high shells, defenses often bring a safety down after the snap as the extra run defender. If he comes, the deep area he left is open.
  • Nobody in particular: the count. On a pre-snap RPO, the quarterback reads no single man. He counts how many defenders are in the box and how many are out over the receivers, and goes where the numbers are.

The run half isn’t always inside zone, though this chapter uses it throughout because you know it best. Duo and split zone carry RPO tags (a tag is a word added to a play call that changes one player’s job) too, and so, less often, do gap runs like counter and power (Gap and Power Runs). Those are rarer because a pulling lineman runs along the line and turns upfield through the hole, and a lineman on the move is the one most likely to be too far downfield if the ball is thrown.

What does the quarterback look for in a post-snap read? The same two things an option quarterback watches in a read key: direction and eyes. A defender who steps forward (or inside) with his eyes in the backfield has chosen the run. A defender who backpedals, widens, or turns his eyes to a receiver has chosen the pass. And the rule for doubt is the same as on most zone reads: the run is the default. The quarterback throws only when the defender has clearly taken himself out of the pass; if he’s unsure, he hands off, because the run was blocked and a hesitant throw is the worst outcome on the field.

Pre-snap RPOs: count, then choose

The simplest RPO is decided before anyone moves. A pre-snap RPO gives the quarterback a run and a quick throw and tells him to pick one from the defense’s alignment (some staffs call these check-with-me plays or alerts). No defender is read after the snap. The quarterback catches the ball and either hands it off or throws it, immediately, to the receiver the numbers favor.

The classic version is the bubble RPO: a run paired with a bubble screen to a slot receiver, the quick screen from The Quick Game in which the receiver bows back and out and catches the ball already running toward the sideline while the receiver outside him blocks. The quarterback counts twice:

  1. The box. How many defenders are in the box, against how many blockers the offense has? With five linemen and a tight end, the offense has six.
  2. Outside. On the bubble side, how many defenders are close enough to stop a bubble (count the ones within about seven yards of the line; a safety twelve yards deep arrives too late), against how many receivers the offense has there?

The defense has to put its players somewhere, so the two counts move against each other. Every defender who walks into the box to stop the run is one fewer outside, and every defender who stays outside over the slot is one fewer in the box.

Show the Python
fig, axes = plt.subplots(1, 2, figsize=(6.8, 3.3))
LAT, WIN = (-6.5, 20.5), (-7.2, 13.6)
for ax, inside in zip(axes, (True, False)):
    dfn = nickel(NB=(3.6, 6.4)) if inside else nickel(NB=(5.0, 9.4))
    p = Play(only(trips(z_w=17.0), -6.5, 20), only(dfn, -6.5, 20), los=35, to_go=None)
    p.highlight("NB", color=style.DEFENSE)
    p.route("H", r.bubble(4.5), side=1)
    p.block("Z", target="RCB")
    if inside:
        p.pass_to("H", t=0.35)
    else:
        p.handoff("RB", t=0.8)
        go_to(p, "RB", (3.2, 2.6), "run", via=[(-4.6, 0.3), (-1.4, 2.4)])
    f = draw(p, ax=ax, lateral=LAT, window=WIN)
    region(f, p, 0.0, 7.0, -4.6, 6.9, style.OFFENSE, alpha=0.07, ls=(0, (3, 2)))
    if inside:
        say(f, p, 9.1, 0.8, "box: 7 defenders\nvs 6 blockers", fontsize=6.8, color=style.INK)
        say(f, p, 3.0, 14.5, "outside: H and Z\nvs 1 cornerback", fontsize=6.8, color=GREEN, weight="bold")
        say(f, p, -5.6, 11.4, "throw the bubble", fontsize=6.8, color=style.BALL, weight="bold")
        point_to(f, p, (9.9, 13.5), (11.5, 9.0), "SS: 12 yards deep,\ntoo far to count", fontsize=6.4)
    else:
        say(f, p, 9.1, 0.8, "box: 6 defenders\nvs 6 blockers", fontsize=6.8, color=GREEN, weight="bold")
        say(f, p, 3.0, 14.5, "outside: 2 vs 2", fontsize=6.8, color=style.INK)
        say(f, p, -6.3, 5.0, "hand off", fontsize=6.8, color=style.OFFENSE_DARK, weight="bold")
    panel_title(ax, "Nickel inside: throw" if inside else "Nickel outside: hand off", 8.8)
fig.tight_layout(w_pad=0.6)
plt.show()
Figure 25.2: The bubble RPO decided by counting, against two alignments of the same nickel defense (the offense’s right side; the box is shaded). Left: the nickel back ($, ringed) has walked inside, next to the tight end. The box now holds seven defenders against six blockers, so a run is a man short; but outside, H and Z face only the cornerback, because the strong safety is twelve yards deep. Two against one: the quarterback throws the bubble to H at once, and Z blocks the cornerback. Right: the nickel back stays out over H. Outside is now two against two, with no advantage, but the box is six against six, a hat on every hat: the quarterback hands off, and H’s bubble is just a decoy.

That’s the whole logic, and it’s worth saying why it’s so hard to defend. The defense wants to put the nickel back in the box against a run-heavy offense: it’s the extra man, the hat math of 02-05. And it wants to put him out over the slot against a passing offense. The bubble RPO says: pick one, and we’ll run the other. The only escape is to have both, which means either bringing a safety down (and playing with one deep defender, which the offense’s next play can attack) or moving after the snap so the count the quarterback made is wrong by the time the ball is out.

The offense pays a little, too. The bubble is a horizontal throw, so its ceiling is low; it gains yards only if the blocker outside wins and the receiver makes the first man miss. It is also a throw that, if it is dropped behind the line, becomes a backward pass and a live ball (The Quick Game has the rule). And it is predictable: an offense that throws the bubble every time the nickel walks in teaches the nickel to walk in and then sprint back out at the snap, which a pre-snap count cannot see. That is the opening for the post-snap RPO.

CautionCommon misconception

“A pre-snap RPO is just an audible.” Not quite. An audible changes the play: the quarterback calls out a new play at the line, and all eleven players have to hear it and change what they do. On a pre-snap RPO nobody changes anything. The line blocks the run, the back runs his track, the receivers run their routes or their screen, and only the quarterback’s last action, hand off or throw, depends on the count. That’s why RPOs are so fast to execute and so hard for a defense to read from the line: there is no new call to hear and no shuffle of feet when the check is made.

Post-snap RPOs: read a defender

A post-snap RPO moves the decision to after the snap. The line blocks the run, the back takes the ball into the mesh, and the quarterback rides him for a beat while he watches one defender. It is the zone read’s mesh from 03-04, with the keep replaced by a throw. If the read defender plays the run, the quarterback pulls the ball and throws behind him. If he plays the pass, the quarterback lets go and the back runs.

One of the NFL’s most common versions, and the flagship of this chapter, is the glance RPO. The run is inside zone (03-02). The pass is a glance (many coaches simply call it a skinny post): the receiver on the side away from the run takes a few hard steps upfield, then bends inside at a shallow angle, a skinny post that ends eight to twelve yards deep. The read is the linebacker on that side, the Will, the one who would fill the backside gaps against inside zone. His run job is to step forward; his pass job, in most zone coverages, is to drop into the window the glance runs into (a passer’s word for the open lane between defenders where a ball can be completed).1

Show the Python
LAT, WIN = (-19.5, 7.5), (-6.8, 14.6)
p = Play(only(trips(), -20, 7), only(nickel("two_high"), -20, 7), los=35, to_go=None)
for k, end in [("LT", (0.25, -4.2)), ("LG", (0.3, -2.0)), ("C", (0.35, 0.3)), ("RG", (0.35, 1.3)),
               ("RT", (0.3, 3.8)), ("Y", (0.35, 4.6))]:
    go_to(p, k, end)
go_to(p, "X", (11.0, -10.4), "route", via=[(4.6, -15.0)])
p.highlight("WILL", color=style.DEFENSE)
f = draw(p, lateral=LAT, window=WIN, width_in=4.6)
read_line(f, p, (-4.6, -0.5), (4.2, -2.1))
branch(f, p, [(4.0, -1.5), (2.6, -0.5)], color=RED, lw=1.7)            # downhill: plays the run
branch(f, p, [(5.0, -2.5), (7.4, -4.6)], color=RED, lw=1.7)            # drops: plays the glance
branch(f, p, [(-5.0, -1.6), (-4.6, -0.2), (-2.0, 1.4), (3.0, 2.9)], color=style.OFFENSE_DARK)
throw(f, p, (-5.0, -0.6), (10.6, -10.2), rad=-0.1)
point_to(f, p, (7.6, 1.8), (2.6, -0.5), "Will plays the run:\nthrow the glance", color=RED, fontsize=6.9)
point_to(f, p, (9.8, -3.4), (7.4, -4.6), "Will drops into\nthe window: hand off", color=RED, fontsize=6.9)
say(f, p, 3.6, 5.2, "give", color=style.OFFENSE_DARK, fontsize=7, weight="bold")
point_to(f, p, (12.6, -15.4), (11.0, -10.6), "X catches it here,\nunder the safety", fontsize=6.6, color=style.BALL)
point_to(f, p, (-4.2, -8.6), (-0.6, -1.7), "LG stays on the 3:\nnobody climbs to the Will", fontsize=6.4)
legend(f.fig, ["block", "route", "give", "pass", "read", "react"], y=0.06, fs=6.8)
plt.show()
Figure 25.3: The glance RPO in one picture (inside zone goes right, away from the read). X runs the glance: a hard stem, then a bend inside to about ten yards, under the deep safety. The quarterback’s eyes are on the Will (ringed) while the ball sits in the back’s belly. If the Will comes downhill to play the run, the window behind him is empty: throw the glance. If he drops into the glance’s path, he has left his run gap: hand off. Nobody blocks the Will on either branch; the left guard stays on the 3-technique.

Figure 25.3 is the whole play in one picture; Figure 25.4 runs it twice, once down each branch, so you can watch the decision happen.

Show the Python
def glance_play(throw_it):
    p = Play(trips(), nickel("two_high"), los=35, to_go=None)
    # defensive line engages; the Mike flows to the run either way
    go_to(p, "BE", (1.0, -4.3), "path", speed=0.6)
    go_to(p, "BT", (1.2, -2.2), "path", speed=0.8)
    go_to(p, "PT", (1.2, 0.8), "path", speed=0.8)
    go_to(p, "PE", (1.1, 4.0), "path", speed=0.6)
    # inside zone right, the line staying on the line of scrimmage until the decision
    go_to(p, "LT", (0.25, -4.2), speed=1.2)
    go_to(p, "LG", (0.3, -2.0), speed=1.6, via=[(-0.4, -1.8)])
    go_to(p, "C", (0.35, 0.3), speed=1.6)
    go_to(p, "RG", (0.35, 1.25), speed=1.6, via=[(-0.4, 1.8)])
    go_to(p, "RT", (0.3, 3.75), speed=1.6)
    go_to(p, "Y", (0.4, 4.6), speed=1.2)
    # X: the glance, a skinny post bending inside at about five yards
    go_to(p, "X", (11.2, -10.4), "route", via=[(4.6, -15.0)])
    go_to(p, "LCB", (13.0, -16.2), "path", speed=4.5, delay=0.3)         # quarters: carries X vertically, then sits
    go_to(p, "FS", (15.0, -6.6), "path", speed=1.6, delay=0.4)           # deep half, settling
    go_to(p, "QB", (-5.3, 0.1), "path", speed=0.6)
    go_to(p, "RB", (-4.2, 0.6), "path" if throw_it else "run", speed=3.6, delay=0.2, via=[(-4.6, -0.6)])
    if throw_it:
        go_to(p, "WILL", (2.8, -1.0), "path", speed=3.4, delay=0.15)    # triggers downhill
        go_to(p, "MIKE", (3.2, 1.4), "path", speed=2.2, delay=0.25)
        go_to(p, "RB", (1.2, 2.6), "path", speed=6.0, via=[(-1.4, 2.0)])  # the fake
        p.pass_to("X", t=0.85)
        go_to(p, "RG", (0.6, 1.6), speed=1.0, delay=0.1)                 # holds the combo: no climbing
    else:
        go_to(p, "WILL", (6.6, -5.0), "path", speed=2.2, delay=0.2)      # sits, then sinks under the glance
        go_to(p, "MIKE", (3.6, 2.6), "path", speed=2.2, delay=0.25)
        p.handoff("RB", t=0.75)
        go_to(p, "RB", (7.0, -1.6), "run", speed=6.6, via=[(-1.4, 1.4), (1.6, 0.0)])   # cuts back where the Will was
        go_to(p, "RG", (2.4, 2.2), speed=3.2, delay=0.4)                 # only after the handoff (about 1.2 s): climbs to the Mike
        go_to(p, "QB", (-3.6, -3.6), "path", speed=4.0, delay=0.15)       # carries out a fake
    return p


TIMES_G = [0.0, 0.8, 1.9]
NOTES_G = ["Snap: the line steps right;\nQ eyes the Will", "Will comes downhill:\nQ pulls the ball and throws",
           "X catches the glance in\nthe space the Will left",
           "Same snap, same look", "Will drops into the\nglance's window: Q hands off", "R (dark path) cuts back\ninto the gap the Will left"]
rpo_animation([glance_play(True), glance_play(False)], TIMES_G, lateral=(-17.6, 6.0), window=(-6.4, 15.2),
              notes=NOTES_G, rows=["Will plays run: throw", "Will plays pass: give"],
              rings=(("WILL", style.DEFENSE),))
Figure 25.4: The glance RPO, two branches of one play (inside zone to the right; the quarterback reads the Will linebacker, ringed in orange; the green ring follows the ball, and the ball carrier’s path is drawn darker). Top, the Will plays the run: at the mesh he is already two steps downhill into the backside gap, so the quarterback pulls the ball and throws the glance to X, who has bent inside into the space behind where the Will was and in front of the deep safety; the back carries out his fake without the ball. Bottom, the Will plays the pass: he sits and drifts back toward the glance’s window, so the quarterback hands off, inside zone runs against five box defenders with six blockers, and the back cuts back into the gap the Will has left. The linemen block exactly the same way in both rows until the ball is gone (they can’t see the mesh), and no lineman is more than a yard past the line when the decision is made. Simulated, BDB-format; the trips side of the formation is outside the picture.

If the Will plays the run (top row of Figure 25.4), he has done what inside zone teaches him: see the line step, see the back coming, fill the gap. Against a plain handoff, that fill is how linebackers make tackles for no gain. Against the glance RPO it empties the zone behind him. The quarterback pulls the ball, the back carries out his fake (which still holds the other linebackers for a moment), and the throw goes into the space between where the Will now is and where the safety is. The receiver catches it eight to twelve yards downfield, often running.

If the Will plays the pass (bottom row), he sits or sinks to take the glance away. Fine: now he isn’t filling his gap, and inside zone is running against five box defenders with six blockers. Count the hats again. The defense started with six in the box against six blockers, an even count. The read took one of those six out of the run without anyone blocking him, so the left guard, who on a plain inside zone would come off the 3-technique and climb to the Will, can simply stay on his block.

That last point is the quiet genius of the post-snap RPO, and it’s the same as the option’s: the read is a block. On plain inside zone, the backside linebacker is somebody’s job, usually a guard who must come off his first block at just the right moment and travel four or five yards to find him. On the glance RPO, nobody has to go get the Will. Whatever he does, he has taken himself out of one of the two plays.

What can go wrong? Three things, and the defense will try all of them:

  • The defender lies. A linebacker who takes one hard step forward and then drops sells “run” to the quarterback and still takes the glance away. Coaches call this a fake fill or a bluff (on the edge, it’s the slow-play of 03-04); a quarterback reading a lie throws into a defender. The cure is the run-as-default rule: throw only on a defender who has clearly committed, two steps and shoulders turned, not one step.
  • Someone else takes the window. If the Will fills the run and a safety rotates down (shows deep before the snap, then comes down toward the line after it) into the space he left, the read was right and the throw is still covered. This is the RPO version of the scrape exchange (03-04), and it’s coming in the defense section below.
  • The clock. The decision has to be made at the mesh, inside about two-thirds of a second, and the ball has to be out right after, for a reason that is about rules rather than football. Hold that thought until The one-yard rule below.

The slant RPO: reading the overhang

The glance reads a linebacker. The slant RPO reads the defender in the apex, and it is the purest picture of the conflict defender in this book.

Show the Python
LAT, WIN = (-20.5, 6.8), (-7.0, 14.6)
dfn = only(nickel("single_high", LCB=(6.5, -16.4), SS=(5.0, -9.6)), -20, 7)
p = Play(only(trips(), -20, 7), dfn, los=35, to_go=None)
for k, end in [("LT", (0.25, -4.2)), ("LG", (0.3, -2.2)), ("C", (0.35, 0.3)), ("RG", (0.35, 1.3)),
               ("RT", (0.3, 3.8)), ("Y", (0.35, 4.6))]:
    go_to(p, k, end)
p.route("X", r.slant(stem=2.0, length=10.0))
p.highlight("SS", color=style.DEFENSE)
f = draw(p, lateral=LAT, window=WIN, width_in=5.9)
read_line(f, p, (-4.6, -0.5), (4.3, -8.9))
branch(f, p, [(4.3, -9.0), (2.6, -7.9)], color=RED, lw=1.7)
branch(f, p, [(5.4, -10.0), (5.6, -12.4)], color=RED, lw=1.7)
branch(f, p, [(-5.0, -1.6), (-4.6, -0.2), (-2.0, 1.4), (3.0, 2.9)], color=style.OFFENSE_DARK)
throw(f, p, (-5.0, -0.6), (7.4, -7.6), rad=-0.12)
point_to(f, p, (0.6, -11.6), (2.6, -7.9), "plays the run:\nthrow", color=RED, fontsize=6.9)
point_to(f, p, (10.4, -12.6), (5.6, -12.4), "walls the slant:\nhand off", color=RED, fontsize=6.9)
say(f, p, 3.4, 4.4, "give", color=style.OFFENSE_DARK, fontsize=7, weight="bold")
point_to(f, p, (2.6, -17.0), (6.5, -16.4), "CB: off and outside,\nconceding the slant", fontsize=6.6)
point_to(f, p, (10.8, -4.0), (12.9, -0.5), "one deep safety", fontsize=6.6)
fig = f.fig
legend(fig, ["block", "route", "give", "pass", "read", "react"], y=0.06, fs=6.8)
plt.show()
Figure 25.5: The slant RPO (the single-receiver side; inside zone goes away to the right). The defense is one-high, with the strong safety walked down as the overhang defender (ringed), halfway between the left tackle and X. X runs a quick slant whose path, five yards past the line, crosses almost exactly the spot where the safety stands. The quarterback reads him at the mesh. If the safety comes forward and inside to help against the run (the red arrow pointing back toward the line), the slant arrives in the spot he has just left: throw. If he holds his depth and widens a step to meet the slant before it gets inside him, walling it off (the red arrow pointing out toward X), the slant is covered, but the backside of the run has lost its extra defender: hand off. Either way the cornerback, playing off and outside, is too far away to help on the slant.

The safety in Figure 25.5 is in the worst seat in football. The defense put him there on purpose. With three receivers to the other side, it has rotated weak: instead of sending extra defenders toward the trips, it walked a safety down on the single-receiver side, so that it still has a spare run defender there (the “+1” of 02-05: one run defender nobody blocks), and accepted three-on-three against the trips with the free safety over the top. The walked-down safety has to be that spare run defender and also take away the quick throw to X. Watch how the offense has arranged the field so that his two jobs point in opposite directions: the run’s cutback lane is inside and forward of him, the slant’s landing spot is at him, and he can’t be in both places at the moment the ball is decided.

The cornerback matters too. The slant is a throw the corner can take away by lining up with inside leverage (on the receiver’s inside shoulder, the term from The Quick Game). If he does, the quarterback’s pre-snap look tells him the slant is dead, and good RPO offenses carry a second answer to that side (a bubble or a quick out paired with the slant) so the read still has two branches. This is where pre-snap and post-snap RPOs meet: the quarterback uses the pre-snap look to choose which pass is live and the post-snap read to choose whether to throw it.

Stick and the pop pass: two more throws behind the run

The same idea works with almost any quick throw that lands behind a run defender. Two you’ll see every weekend:

  • The stick RPO. Stick from The Quick Game puts a receiver at about six yards, settling in front of the quarterback, with a second receiver in the flat. As an RPO the flat route is usually dropped, and the outside receiver clears deep instead: the run replaces the flat as the second option. The quarterback reads the defender over the slot (the nickel or the overhang). If he triggers (commits hard, downhill, to the run), the stick is open behind him: throw. If he widens or sinks under the stick, hand off. Notice that “widens” now means the opposite of what it meant in 04-03. There, a nickel who widened was chasing the flat route and left the stick open; here there is no flat route to chase, so a nickel who widens is covering the stick, and the run is the open answer.
  • The pop pass. A pop pass uses a player who looks like a run blocker. The tight end steps as if to block the end or a linebacker, then releases (“pops”) straight up the field into the space behind linebackers who have stepped up to stop the run. The read is usually the linebacker over him. The pop is the RPO’s cleverest trick, because linebackers are taught to read the tight end: a tight end who blocks means run. On the pop pass, the tight end shows them exactly that, for one beat, and they come.
Show the Python
fig, axes = plt.subplots(1, 2, figsize=(6.8, 3.6))
LAT, WIN = (-6.0, 19.5), (-7.0, 15.0)
# stick RPO
p = Play(only(trips(), -7, 20), only(nickel("two_high"), -7, 20), los=35, to_go=None)
for k, end in [("LT", (0.25, -3.6)), ("LG", (0.3, -1.6)), ("C", (0.35, 0.6)), ("RG", (0.35, 1.4)),
               ("RT", (0.3, 3.8)), ("Y", (0.35, 4.8))]:
    go_to(p, k, end)
p.route("H", r.stick(6.0))
p.route("Z", r.go(13.0))
p.highlight("NB", color=style.DEFENSE)
f = draw(p, ax=axes[0], lateral=LAT, window=WIN)
read_line(f, p, (-4.6, 0.4), (4.6, 7.0))
branch(f, p, [(-5.0, -1.6), (-4.6, -0.2), (-2.0, 1.6), (3.0, 2.9)], color=style.OFFENSE_DARK)
branch(f, p, [(4.4, 6.8), (2.8, 5.6)], color=RED, lw=1.6)
branch(f, p, [(5.9, 7.6), (7.4, 8.4)], color=RED, lw=1.6)
throw(f, p, (-5.0, 0.4), (4.6, 9.3), rad=0.05)
point_to(f, p, (7.6, 3.4), (2.8, 5.6), "plays the\nrun: throw", color=RED, fontsize=6.7)
point_to(f, p, (9.2, 11.8), (7.4, 8.4), "sinks:\ngive", color=RED, fontsize=6.7)
say(f, p, 3.4, -0.6, "give", color=style.OFFENSE_DARK, fontsize=6.7, weight="bold")
panel_title(axes[0], "Stick RPO: read the nickel", 8.8)
# pop pass
off = trips(rb_w=1.6)
p = Play(only(off, -10, 13), only(nickel("two_high"), -10, 13), los=35, to_go=None)
for k, end in [("LT", (0.3, -4.4)), ("LG", (0.3, -2.8)), ("C", (0.35, -0.9)), ("RG", (0.35, 0.3)),
               ("RT", (0.3, 2.4))]:
    go_to(p, k, end)
go_to(p, "Y", (7.6, 3.6), "route", via=[(-0.2, 4.4), (1.2, 4.5)])
p.route("H", r.go(12.0))
go_to(p, "MIKE", (2.8, 0.3), "path")
go_to(p, "NB", (6.5, 10.0), "path")          # the nickel widens with H's vertical
p.highlight("MIKE", color=style.DEFENSE)
f = draw(p, ax=axes[1], lateral=(-10.0, 15.5), window=WIN)
read_line(f, p, (-4.6, 0.3), (2.8, 0.4))
branch(f, p, [(-5.0, 1.6), (-4.6, 0.2), (-2.0, -1.6), (2.6, -3.4)], color=style.OFFENSE_DARK)
throw(f, p, (-5.0, 0.3), (7.4, 3.6), rad=-0.12)
point_to(f, p, (-3.2, 7.4), (0.6, 4.5), "Y fakes a block\nfor one beat", fontsize=6.6)
point_to(f, p, (9.8, -0.6), (7.6, 3.6), "pop: behind\nthe Mike", fontsize=6.6, color=style.BALL)
point_to(f, p, (3.4, -6.4), (2.8, 0.3), "Mike\nsteps up", fontsize=6.4, color=RED, shrink=9)
panel_title(axes[1], "Pop pass: read the Mike", 8.8)
fig.tight_layout(w_pad=0.6, rect=(0, 0.07, 1, 1))
legend(fig, ["block", "route", "give", "pass", "read", "react"], y=0.075, fs=6.8)
plt.show()
Figure 25.6: Two more pass branches behind an inside-zone run. Left, the stick RPO to the trips side: H runs a stick to six yards and Z clears the cornerback deep; there is no flat route, because the run is the second option. The quarterback reads the nickel back ($, ringed): if he steps up to play the run coming toward him, he leaves the stick open; if he widens and sinks under it, hand off. Right, the pop pass, run going left: the tight end Y steps down as if blocking the 7-technique, then releases up the field behind the Mike linebacker (ringed), who reads Y’s block as run and steps up into the hole; the quarterback pulls and throws to Y about seven yards downfield. Y is an eligible receiver, so his blocking fake can go anywhere; it’s the five linemen who must stay near the line.

The pop pass also shows why RPOs and tight ends get along. A tight end is eligible, so his fake block can last as long as the play needs and his release can go anywhere. The five linemen can’t do the same: they are ineligible receivers, and one rule about where they may be when a pass is thrown shapes every RPO in this chapter.

The one-yard rule: why NFL RPOs look different

In football, only some players may catch a forward pass. The five interior linemen are ineligible receivers (The Twenty-Two), and the rules don’t only stop them catching the ball, they stop them running downfield before it is thrown. Imagine the game without that limit: every pass play could begin with the whole line charging out and blocking linebackers, exactly as on a run. A linebacker could never tell run from pass, so he could never safely drop into coverage, and the receivers behind him would be catching passes in space that had been blocked for them. The rule keeps a pass play looking like a pass play. The foul is called illegal man downfield, or ineligible downfield (the referee’s announcement is often “ineligible man downfield”).

Here is the NFL’s version, as written in the 2026 rulebook:2

  • On a play on which a legal forward pass is thrown, it is a foul if the entire body of an ineligible player is more than one yard beyond the line of scrimmage before the pass is thrown.
  • Exception: a lineman who makes contact with a defender within a yard of the line, on his initial charge, may be driven or may drive his man further downfield while still blocking him. If they come apart beyond the yard, he may stay where he is, move sideways or back off until the ball is thrown (some line coaches call it freezing). What he may not do is let go of that defender and keep moving forward, or go past a yard without having touched anyone.
  • Penalty: five yards from the previous spot. A completed pass is wiped out; there is no loss of down.

Now set that against the run game. On inside zone, linemen double-team a defensive lineman and then one of them climbs: he lets go and runs to a linebacker four or five yards deep. That climb is the heart of zone blocking, and on a pass play it is exactly what the rule forbids: letting go of a defender inside one yard and then advancing.

College football draws the line somewhere else. Under the NCAA rule, no originally ineligible player may be more than three yards beyond the neutral zone (the strip the length of the ball between the two lines) until a legal forward pass that crosses the neutral zone has been thrown, also a five-yard penalty. (College judges any part of the body, the NFL the entire body.)3 Three yards is room for a guard to release toward a linebacker who has stepped up. Actually blocking him that far downfield before a pass that crosses the neutral zone is still offensive pass interference in college, unless the block began on the lineman’s first charge within a yard of the neutral zone. And the “crosses the neutral zone” clause means a pass caught behind the line (a bubble, a now screen) doesn’t trigger either college rule at all.

The NFL rule has no such clause: it applies to any legal forward pass, including one caught behind the line, which is how Play-Action, Bootlegs, and Screens described NFL screens too. The one throw it doesn’t touch is a backward pass. A bubble thrown even slightly backward is a lateral, not a forward pass, so nobody can be “ineligible downfield” on it; the price, as The Quick Game warned, is that a dropped lateral is a live ball.

Show the Python
fig, axes = plt.subplots(1, 2, figsize=(6.8, 3.6))
LAT, WIN = (-17.5, 8.0), (-6.4, 14.2)
for ax, college in zip(axes, (False, True)):
    base = only(trips(), -18, 7)
    common = {"QB": (-5.4, -1.2), "RB": (-3.6, 0.9), "X": (7.2, -13.2), "Y": (0.0, 5.2)}
    dcommon = {"LCB": (11.0, -16.0), "FS": (12.6, -8.2)}
    if college:
        spots = {"LT": (0.6, -3.9), "LG": (0.6, -2.3), "C": (0.6, 0.4), "RG": (2.4, 1.7), "RT": (0.6, 3.6)}
        dspots = {"BE": (1.6, -4.1), "BT": (1.6, -2.3), "PT": (1.6, 0.45), "PE": (1.6, 4.0),
                  "WILL": (3.4, -1.0), "MIKE": (5.0, 2.0)}
    else:
        spots = {"LT": (1.9, -3.9), "LG": (0.3, -2.2), "C": (0.3, -0.4), "RG": (0.3, 1.4), "RT": (0.3, 3.2)}
        dspots = {"BE": (3.1, -3.9), "BT": (1.1, -2.4), "PT": (1.1, 0.5), "PE": (1.1, 4.2),
                  "WILL": (3.4, -0.9), "MIKE": (4.6, 2.4)}
    spots.update(common)
    dspots.update(dcommon)
    players = [q.moved(d=spots[q.key][0], w=spots[q.key][1]) if q.key in spots else q for q in base]
    dfn = [q.moved(d=dspots[q.key][0], w=dspots[q.key][1]) if q.key in dspots else q
           for q in only(nickel("two_high"), -18, 7)]
    p = Play(players, dfn, los=35, to_go=None)
    f = draw(p, ax=ax, lateral=LAT, window=WIN)
    lim = 3.3 if college else 1.0
    region(f, p, 0.0, lim, -17.4, 7.9, GREEN, alpha=0.13, lw=0.8)
    say(f, p, lim + 0.8, -11.6, ("3 yards past the\nneutral zone" if college else "1 yard past the line"),
        color=GREEN, fontsize=6.8, weight="bold")
    throw(f, p, (-5.4, -1.2), (11.2, -11.6), rad=-0.1)
    if college:
        point_to(f, p, (7.6, 3.6), (2.4, 1.7), "RG climbing to\nthe Mike: legal", fontsize=6.4, color=GREEN)
    else:
        x, y = _P(f, p, 3.2, 1.2)
        ax.add_patch(Circle((float(x), float(y)), 0.75, fc="none", ec=style.OFFENSE, lw=1.2, ls=(0, (2, 1.5)),
                            zorder=8))
        tackle_x(f, p, 3.2, 1.2, ms=7)
        point_to(f, p, (7.6, 3.6), (3.2, 1.2), "a guard who climbs\nhere: flag", fontsize=6.4, color=RED)
        point_to(f, p, (-2.6, 4.9), (0.5, 0.7), "C + RG still\ndouble the 1", fontsize=6.4)
        point_to(f, p, (6.6, -5.2), (2.3, -4.5), "LT drove his man\nback 2 yards: legal", fontsize=6.4, shrink=2)
    panel_title(ax, "College: 3 yards" if college else "NFL: 1 yard", 9)
fig.tight_layout(w_pad=0.6)
plt.show()
Figure 25.7: The same glance RPO at the moment the ball is thrown, under the two rulebooks (the left half of the line; the shaded strip is how far past the line of scrimmage an ineligible lineman may be when the pass leaves the quarterback’s hand). Left, NFL: one yard. The linemen are still on the line, holding their first blocks; the center and right guard are still doubling the 1-technique. A right guard who had already come off that double team and climbed to the Mike (hollow circle) would be three yards downfield: flag. The left tackle, by contrast, engaged the end at the line and has driven him two yards back: legal, because he never let go. Right, college: three yards beyond the neutral zone. The right guard has come off the double team and is climbing toward the Mike, about two and a half yards past the line when the ball leaves: no foul in college (in the NFL it would be). Every defensive lineman is still blocked, and the glance goes over the linebackers. (Had the guard already been blocking the Mike that far downfield, college could call offensive pass interference instead.)

That difference is the single biggest reason an NFL RPO looks different from a college one:

  • NFL RPO throws are quick, and most are short. The decision is made at the mesh and the ball is out within about a second of the snap, before any lineman has had time to drift past a yard. And a ball that must leave in a second can’t go far, because a receiver can’t get far in a second: running full speed from a standing start, he is only about five yards downfield when it’s thrown. A deep route needs two and a half seconds or more to develop, and by then a run-blocking line is well past a yard. So NFL RPOs throw to routes that are open early: slants, glances and sticks a few yards past the line, and bubbles and now screens that come out at once (the bubble, thrown slightly backward, isn’t a forward pass at all). The data below shows how strong this is.
  • NFL RPO blocking stays at the line. Instead of climbing, linemen hold their first blocks, or the offense uses blocking that naturally stays on the line (down blocks, double teams that don’t release) and lets the read handle the linebacker. The glance RPO is popular in the NFL for exactly this reason: the read takes care of the man the climbing guard would have blocked.
  • College RPOs can be “run first, throw later.” With three yards and no limit at all on throws behind the line, college linemen can run-block for longer and drift up to three yards downfield, the quarterback can ride the mesh longer, and the pass can go farther downfield: seams and posts over linebackers who have stepped up to the run.

Why hasn’t college changed? It nearly did. In 2015 the NCAA’s rules committee proposed cutting its limit to one yard, and the oversight panel tabled the proposal; the rule is still three yards for 2026.4 The NFL has never had college’s three-yard cushion: its rulebook still says one yard in 2026. That leaves officials judging a one-yard line at full speed on every RPO, and RPOs put the rule under strain on every snap: an RPO whose throw comes out a beat late is exactly the play on which a run-blocking lineman has drifted too far. You will see in the numbers that RPOs draw far more than their share of the flags.

NoteGo deeper

Receivers who block too early. Receivers are eligible, so the one-yard limit doesn’t bind them, but a cousin rule does: in the NFL, blocking a defender more than a yard past the line “clearly prior” to a pass is offensive pass interference, and pass interference can occur “regardless of whether [the pass] crosses the line.”5 So on a bubble RPO the receiver blocking the cornerback wants his block to land as the ball leaves the quarterback’s hand, one more reason the bubble comes out at once. Judging “clearly prior” at full speed is hard; Bill Belichick, asked about RPOs in 2019, called the downfield-blocking question “a tough call for the officials.”6

Three choices on one snap

Nothing stops an offense from putting the zone read and an RPO on the same snap. The quarterback reads two defenders in order, exactly like the triple option of 03-04, with a pass in the pitch man’s role:

  1. At the mesh, read the backside end: if he stays wide, give to the back; if he crashes on the back, pull the ball.
  2. Now moving sideways along the line with the ball, read the overhang defender: if he comes up to tackle the quarterback, throw the bubble to the receiver outside him, before crossing the line (a forward pass thrown from beyond the line of scrimmage is illegal); if he stays with the receiver, tuck the ball and run.

In practice the throw on a triple RPO is almost always a bubble or a flare, a ball thrown sideways or slightly backward, not a slant, for the reason of the previous section: by the time the quarterback has pulled the ball and taken a few steps, a second and a half has gone by, and a run-blocking line is past a yard. College offenses called these triple RPOs or simply packaged plays, and they were common in the shotgun spread offenses of the 2000s and 2010s, where the three-yard rule left more room. They are rarer in the NFL, for two reasons: the quarterback carries the ball into the second level on the keep, where he takes hits, and every extra beat of reading is a beat in which the linemen are run blocking further from the line.

Where RPOs came from

Nobody invented the RPO in one moment, and several people say they did. What is not disputed is the ingredients: the option’s habit of reading a defender instead of blocking him, and the spread offense’s habit of giving the quarterback a quick throw to whichever side the defense left light. Put a quick throw on a running play and you have it.

  • Air Raid tags and the 1990s. Coaches in Hal Mumme and Mike Leach’s Air Raid tradition (a spread, pass-first offense built on a handful of plays repeated endlessly; 04-08 has it), at Kentucky in the late 1990s and Leach’s Oklahoma in 1999, tagged quick screens onto runs so the quarterback could throw if the defense overloaded the box. Rich Rodriguez, whose zone read grew out of a fumbled practice handoff at Glenville State (03-04), built quick slants and bubbles into his zone-read offense at Tulane and Clemson.7
  • Claims to the invention. Urban Meyer says his Utah teams found it by accident in the early 2000s, when a receiver missed a block and ran a bubble instead;8 Brett Favre says he started doing it on his own, bored with handing off, before he had told his coach.9 Treat both as what they are: stories coaches tell about a thing that was in the air.
  • The college spread, 2000s–2010s. Spread offenses made packaged plays routine. Chip Kelly’s Oregon ran them fast and often enough that when Kelly took over the Eagles in 2013, Washington’s defensive coordinator Jim Haslett prepared to face Kelly’s new team by watching 23 or 24 Oregon films.10 At Auburn, Gus Malzahn’s “still-emerging RPO game,” in CBS writer Dennis Dodd’s words, helped beat Alabama in the 2013 “Kick Six” game; Dodd credits Malzahn with having “refined and perfected the modern run-pass option.”11 And when Robert Griffin III arrived in Washington in 2012 from Art Briles’s Baylor, Mike Shanahan borrowed Baylor’s packaged plays. After Griffin’s first game, Shanahan put it this way: “we had running plays called and all of a sudden they gave us certain looks to take the running game away, and we threw a couple of bubbles early.”12 The full lineage, from the Wing-T and the wishbone through the spread option, is in The College Laboratory.
  • The NFL boom, 2016–2018. The tree that made RPOs a fixture of NFL offense is Andy Reid’s. By Pro Football Focus’s charting, the share of NFL plays that were RPOs roughly doubled from about 4% in 2016 to about 8% in 2018; Reid’s Chiefs led the league in RPO usage in 2017 (17.1% of their plays) and 2018 (25.0%), and Doug Pederson, who had been Reid’s offensive coordinator in Kansas City from 2013 to 2015, ranked second with the Eagles in 2017 and won a Super Bowl with them (see the film room). Matt Nagy, Pederson’s successor in Kansas City, took the idea to Chicago, where the Bears went from 2.7% RPOs in 2017 to 19.2% in his first season, 2018.13
  • Since then. PFF’s league-wide rate kept climbing to a peak of about 10.5% of plays in 2023, then fell to about 9.0% in 2024 and 8.0% in 2025.14 RPOs stopped being a trend and became furniture: almost every NFL offense has a handful in every game plan.
NoteFilm room: Philadelphia Eagles 2017

The 2017 Eagles are the RPO’s breakthrough on the NFL’s biggest stage. Doug Pederson built RPOs into the offense for Carson Wentz, and when Wentz tore his ACL against the Rams in December, he leaned on them even harder for the backup, Nick Foles. The logic was the RPO’s sales pitch in miniature: Foles was a less instinctive improviser than Wentz, so the coaches gave him throws whose answer the defense handed him at the snap. During the playoff bye they went back to film of Foles’s 2013 season under Chip Kelly (27 touchdown passes, two interceptions), with Kelly’s Oregon-bred packaged plays, to find the quick, defined reads that suited him. “It’s centered around accurate throwing, good decision-making and good execution,” offensive coordinator Frank Reich said in January 2018. “When we use it, Nick has shown a great aptitude of doing that very well.” By PFF’s count, Foles completed 93.9% of his RPO throws that season. He completed 26 of 33 passes for 352 yards and three touchdowns in the 38–7 NFC Championship win over Minnesota, and two weeks later, on February 4, 2018, in Minneapolis, he threw for 373 yards and three touchdowns (and caught one) as the Eagles beat New England 41–33 in Super Bowl LII. He was the game’s MVP.15

What to notice on the Eagles’ playoff film: how often the ball came out of Foles’s hand almost at the mesh, on quick slants, glances and bubbles, before the linebackers had finished stepping toward the run fake; and how the line’s first step was identical on the handoffs and the throws. Defenses that sat back to take away the throws got Jay Ajayi, LeGarrette Blount and Corey Clement running against light boxes.

NoteFilm room: Kansas City Chiefs, 2017 to 2025

Andy Reid’s Chiefs have used RPOs more, and for longer, than any other NFL team: first in the league in PFF’s count in 2017 and 2018, and first again in 2025 in FTN’s charting (see the next section). The quarterback changed from Alex Smith to Patrick Mahomes; the idea didn’t. Reid’s RPOs are built for his receivers’ speed, glances and slants behind linebackers, quick screens to the fastest player on the field, and since 2020 Mahomes has thrown more passes on RPOs than any other NFL quarterback.16 The full Reid story is in the Chiefs case study.

What to notice: how short Reid’s RPO throws are. In 2025 FTN charted 150 Chiefs RPOs: 88 handoffs and 62 throws, more than half of them on first down and all but two from the shotgun. The median throw was caught two yards behind the line, and the most frequent targets were Rashee Rice (17) and Xavier Worthy (11). The Chiefs use the RPO to put the ball in their fastest players’ hands in space, with a run already blocked if the defense takes that away.17

NoteFilm room: Jalen Hurts’s Eagles, 2022

The RPO came back to Philadelphia in a different form with Jalen Hurts, whose running made every read harder to defend. In 2022, the season the Eagles reached Super Bowl LVII, FTN’s charting has them using RPOs on a larger share of their plays than any other team. Their offensive coordinator that year, Shane Steichen, became the Colts’ head coach in 2023, and the 2023 Colts used RPOs on a bigger share of their plays than anyone in the four seasons of FTN data: a scheme travelling with its coach.18 The Eagles’ RPOs, the run game and the tush push are the subject of their own case study.

One drive to look up: the Eagles’ opening drive of Super Bowl LVII (Feb. 12, 2023, against Kansas City). On first-and-10 from the Chiefs’ 47 with 12:31 left in the first quarter, Hurts threw to the tight end Dallas Goedert for 13 yards; on the next snap, without a huddle, he hit DeVonta Smith for 23. FTN charted both throws as RPOs. It also charted them as play-action, and it charted Hurts’s 45-yard touchdown to A.J. Brown early in the second quarter as an RPO too, a reminder of how blurry the line between the two families is in charting.19

What to notice: how Hurts’s legs change the read. With a quarterback who can also keep on a zone read, the backside end and the overhang defender both have to respect him, so a single snap can carry a give, a keep and a throw.

What the numbers say

The NFL’s play-by-play doesn’t say whether a play was an RPO: a handoff is a handoff and a pass is a pass. But FTN’s charters, who watch every NFL play, flag RPOs, and the flag is in the nflverse data from 2022 on. The numbers come with a caveat that is worth learning once: charting services disagree. PFF’s rates for the same seasons run well above FTN’s (about 8 to 10.5 percent of plays against FTN’s 4 to 6), because deciding from film whether a handoff “was” an RPO (did a receiver run a real route? was the quarterback reading someone?) is a judgment, and charters judge differently. Use one series at a time, and compare teams within it.

Show the Python
from gridiron.data import nfl, pbp

style.apply()
SEASONS = [2022, 2023, 2024, 2025]
ftn = nfl().load_ftn_charting(SEASONS).to_pandas().rename(
    columns={"nflverse_game_id": "game_id", "nflverse_play_id": "play_id"})
ftn["is_rpo"] = ftn["is_rpo"].fillna(False).astype(bool)
cols = ["season", "season_type", "game_id", "play_id", "posteam", "play_type", "epa", "success", "air_yards",
        "down", "penalty_type", "complete_pass", "shotgun"]
allp = pbp(SEASONS, columns=cols)
allp = allp[allp["season_type"] == "REG"]
reg = allp[allp["play_type"].isin(["pass", "run"]) & allp["down"].notna()]
m = reg.merge(ftn[["game_id", "play_id", "is_rpo", "n_defense_box", "is_screen_pass"]],
              on=["game_id", "play_id"], how="inner")
rpo = m[m["is_rpo"]]
rate = m.groupby("season")["is_rpo"].mean()
team25 = m[m["season"] == 2025].groupby("posteam")["is_rpo"].mean().sort_values()
lead = {s: m[m["season"] == s].groupby("posteam")["is_rpo"].mean().sort_values(ascending=False)
        for s in SEASONS}
thrown = rpo[(rpo["play_type"] == "pass") & rpo["air_yards"].notna()]
other = m[(~m["is_rpo"]) & (m["play_type"] == "pass") & m["air_yards"].notna()]

fig, axes = plt.subplots(1, 2, figsize=(6.8, 4.9), gridspec_kw=dict(width_ratios=[1.0, 1.25]))
ax = axes[0]
hi = {"KC"}
ax.barh(range(len(team25)), team25.values * 100,
        color=[style.SERIES[0] if t in hi else "#9ec5f4" for t in team25.index], height=0.72)
ax.set_yticks(range(len(team25)))
ax.set_yticklabels(team25.index, fontsize=6.2)
ax.axvline(rate[2025] * 100, color=style.INK_2, lw=0.9, ls=(0, (3, 2)))
ax.text(rate[2025] * 100 + 0.3, 1.0, f"league\n{rate[2025]:.1%}", fontsize=6.8, color=style.INK_2, va="center")
ax.set_xlabel("% of plays charted as RPO, 2025", fontsize=7.5)
ax.set_title("RPO rate by team, 2025", fontsize=9.5)
ax.grid(axis="y", visible=False)
ax.tick_params(axis="x", labelsize=7)
ax.set_ylim(-0.7, len(team25) - 0.3)
ax = axes[1]
bins = np.arange(-8, 31)
for d_, name, col in [(thrown, "RPO throws", style.SERIES[0]), (other, "All other passes", style.SERIES[1])]:
    a = d_["air_yards"].clip(-8, 30)
    share = a.round().value_counts(normalize=True).reindex(bins, fill_value=0) * 100
    ax.step(bins, share.values, where="mid", color=col, lw=1.8, label=name)
ax.axvspan(-8.5, 0.5, color=style.GRID, alpha=0.6, zorder=0)
ax.text(-4.0, 15.6, "at or behind\nthe line", fontsize=7, color=style.INK_2, ha="center")
ax.set_xlim(-8.5, 30.5)
ax.set_ylim(0, 17.5)
ax.set_xlabel("air yards (30 = 30 or more)", fontsize=7.5)
ax.set_ylabel("% of throws", fontsize=7.5)
ax.set_title("How far RPO throws go, 2022–2025", fontsize=9.5)
ax.legend(fontsize=7.2, loc="upper right")
ax.tick_params(labelsize=7)
fig.tight_layout(w_pad=1.4)
plt.show()

behind_r, behind_o = (thrown["air_yards"] <= 0).mean(), (other["air_yards"] <= 0).mean()
deep_r, deep_o = (thrown["air_yards"] > 10).mean(), (other["air_yards"] > 10).mean()
gave25 = (rpo.loc[rpo["season"] == 2025, "play_type"] == "run").mean()
gave_all = (rpo["play_type"] == "run").mean()
epa_pass, epa_run = rpo.loc[rpo["play_type"] == "pass", "epa"].mean(), rpo.loc[rpo["play_type"] == "run", "epa"].mean()
comp_r = thrown["complete_pass"].mean()
epa_rpo, epa_non = rpo["epa"].mean(), m.loc[~m["is_rpo"], "epa"].mean()
sr_rpo, sr_non = rpo["success"].mean(), m.loc[~m["is_rpo"], "success"].mean()
idp = allp[allp["penalty_type"].fillna("").eq("Ineligible Downfield Pass")]
idp = idp.merge(ftn[["game_id", "play_id", "is_rpo"]], on=["game_id", "play_id"], how="inner")
idp_n, idp_rpo = len(idp), idp["is_rpo"].mean()
rpo_share = m["is_rpo"].mean()
box_thrown = {k: (rpo.loc[rpo["n_defense_box"] == k, "play_type"] == "pass").mean() for k in (6, 7)}
box_thrown[8] = (rpo.loc[rpo["n_defense_box"] >= 8, "play_type"] == "pass").mean()
kc25, phi22, ind23 = lead[2025].get("KC"), lead[2022].get("PHI"), lead[2023].get("IND")
top = {s: lead[s].index[0] for s in SEASONS}
non = m[~m["is_rpo"]]
epa_npass, epa_nrun = non.loc[non["play_type"] == "pass", "epa"].mean(), non.loc[non["play_type"] == "run", "epa"].mean()
sd_rpo, sd_non = rpo["epa"].std(), non["epa"].std()
gun_rpo = rpo["shotgun"].mean()
rank25 = {t: i + 1 for i, t in enumerate(lead[2025].index)}
Figure 25.8: RPOs in FTN’s NFL charting. Left: the share of each team’s offensive plays charted as RPOs, 2025 regular season (runs and passes, scrambles included, kneel-downs and spikes excluded). Right: how far past the line RPO throws travel in the air, against all other passes, 2022–2025 regular seasons; air yards at or behind zero (shaded) are thrown to a receiver at or behind the line of scrimmage. RPO throws pile up behind the line, the bubbles and now screens that NFL timing favors; other passes spread far downfield. (The bump at the right edge of the ‘all other passes’ line is the last bin, which holds every throw of 30 or more air yards.) Data: nflverse play-by-play joined to FTN charting (load_ftn_charting) via nflreadpy; FTN’s RPO flag is a charter’s judgment, and other services count more RPOs.

Five things to read from Figure 25.8 and the numbers behind it.

RPOs are a small, steady slice of NFL offense. FTN charted 5.0% of plays as RPOs in 2022, 5.3% in 2023, 3.6% in 2024 and 6.2% in 2025 (the 2024 dip is larger than PFF’s and may partly be charting). The leaders are the teams this chapter has already met: PHI in 2022 (13.5%), IND in 2023 (18.3%), and Kansas City in 2025 (14.4%), more than twice the league rate. At the other end, the RPO is plainly a play-caller’s choice, not a law of modern football. The 49ers and Rams, who run mostly from under center with wide-zone blocking, are the bottom two in 2025 (0.7% and 0.5%): 96% of the league’s charted RPOs came from the shotgun, and an offense that lives under center gets the same “make the linebacker step up, then throw behind him” effect from play-action instead (04-05). Yet the 2025 Packers, whose head coach Matt LaFleur comes from the same Shanahan–McVay tree, ranked 5th. And Baltimore, with Lamar Jackson, one of the league’s most dangerous running quarterbacks, sits mid-table (5.6%): a zone read or a designed quarterback run is an option run, and FTN doesn’t count it as an RPO.

Most RPOs are runs. On 69% of the charted RPOs from 2022 to 2025 the ball was handed off (79% in 2025). That is the “run with a safety valve” idea from the Madden callout, in data. It also means the RPO’s main effect is invisible in a box score: its product is the defender who wasn’t in the run fit.

NFL RPO throws are short, and very often behind the line. 57% of RPO throws were caught (or aimed) at or behind the line of scrimmage, against 21% of other passes; only 11% travelled more than ten yards in the air, against 30% of other passes. That shape is the clock at work: the bubbles and now screens come out about half a second after the snap, before any lineman can drift a yard, and a throw beyond the line has to leave so fast that it can’t be far beyond it.

The rule bites. From 2022 to 2025 the NFL called 246 ineligible-downfield fouls on passes in the regular season. 24% of them came on plays FTN charted as RPOs, though RPOs were only 5.0% of plays: per snap, an RPO is many times more likely to draw the flag than any other play.

Per play, RPOs are about average, and steadier. Split by branch, the numbers look dramatic: RPO throws averaged +0.057 EPA with a 79% completion rate, against +0.001 for all other passes; RPO handoffs averaged -0.053, against -0.015 for all other runs. Don’t read that as “the throws are great and the runs are bad.” It is selection, as with the quarterback keeps of 03-04: the quarterback throws precisely when the throw is open and hands off when it isn’t, so the throws are the good half of the reads by construction. Put the halves back together and RPOs come out close to ordinary plays: -0.019 EPA per play against -0.005, and an EPA success rate of 42.8% against 43.5%. What does differ is the spread. The standard deviation of EPA on RPOs is 1.00, against 1.40 on other plays: fewer disasters and fewer explosions, which is what you’d expect from a play that throws only short and only when it’s open.20

The box count shows up, weakly, too: when FTN counted six defenders in the box, 28% of RPOs were thrown; with seven, 33%; with eight or more, 40%. More defenders near the ball, more throws: the counting logic of the bubble RPO, visible in charting that sees only the alignment at the snap.

PFF’s charting, with its larger RPO count, tells a slightly different story: from 2021 to 2025, NFL RPOs gained fewer yards per play than other plays (4.53 against 5.43) but were “successful” more often (52.3% against 48.1%).21 The two series don’t contradict each other so much as measure different things. PFF uses its own success definition, not the EPA-based one above, and its RPO count is about twice FTN’s, so it includes many snaps FTN called plain runs or passes. What both agree on is the shape: RPOs are not big-play machines. They are a low-variance way to gain modest yardage with a run that is always blocked, and whether they beat an ordinary play on efficiency depends on whose charting you trust.

How defenses answer

Defenses have had a decade to study NFL RPOs, and the answers all attack one of the RPO’s three parts: the read, the throw, or the run behind it. This is a preview; the defense’s side gets its full treatment in Run Fits: Gap Integrity, Force, Spill, and Box Math.

Show the Python
LAT, WIN = (-19.0, 6.5), (-7.0, 17.0)
dfn = only(nickel("single_high", LCB=(6.5, -16.4)), -20, 7)
p = Play(only(trips(), -20, 7), dfn, los=35, to_go=None)
for k, end in [("LT", (0.25, -4.2)), ("LG", (0.3, -2.2)), ("C", (0.35, 0.3)), ("RG", (0.35, 1.3)),
               ("RT", (0.3, 3.8)), ("Y", (0.35, 4.6))]:
    go_to(p, k, end)
p.route("X", r.slant(stem=2.0, length=8.0))
go_to(p, "SS", (5.4, -11.0), "path")
go_to(p, "FS", (3.4, -1.4), "path", via=[(9.0, -1.0)])
go_to(p, "RB", (2.0, 0.4), "run", via=[(-4.6, -0.2), (-1.6, 1.4), (0.8, 0.9)])
p.highlight("SS", color=style.DEFENSE)
f = draw(p, lateral=LAT, window=WIN, width_in=5.9)
read_line(f, p, (-4.6, -0.5), (5.2, -8.6))
tackle_x(f, p, 2.6, -0.2)
say(f, p, 3.7, -14.7, "SS walls the slant:\nQ reads 'give'", color=RED, fontsize=6.8)
point_to(f, p, (11.4, 4.0), (8.0, -1.0), "FS comes down into\nthe box at the snap", fontsize=6.8, color=RED)
say(f, p, 15.2, -0.5, "deep middle: empty after the snap", fontsize=6.8, color=style.INK_2)
point_to(f, p, (9.0, -4.8), (2.6, -0.2), "the give meets\nthe safety", fontsize=6.6, shrink=5)
plt.show()
Figure 25.9: Making the read lie: one answer to the slant RPO of the earlier figure. The overhang safety (ringed) is told to play the slant no matter what, and he walls it at the snap, so the quarterback reads ‘give.’ But the free safety, deep in the middle, has a run job on this call, decided before the snap: he comes downhill the instant the ball is snapped, without waiting to read anything, into the backside of the box, the spot the overhang defender would have filled. The read was right and the run still meets an extra defender (the red X). The price: with the free safety in the box, nobody is left in the deep middle.
  • Read the line, not the back. The most useful key a linebacker has against an RPO team is the uncovered guard, the one with no defensive lineman in front of him (03-01). If he climbs hard to the second level, it’s a true run: he couldn’t legally do that on a pass. If he stays at the line, sits, or blocks whoever is in front of him without climbing, it may be an RPO, and the linebacker plays his pass job first. The cost: the key is only as good as the offense’s honesty, and offenses answer with true runs whose guards climb, to punish linebackers who wait.
  • Make the read lie. Give the read defender a rule (always play the pass) and put someone else in his run fit, usually a safety rotating down after the snap (showing deep, then dropping toward the line), as in Figure 25.9. It is the scrape exchange of 03-04 with a safety instead of a linebacker, and it’s built on the post-snap rotations of Disguise and Rotation. It works because the safety’s run job is called: he triggers at the snap without reading anything, so he arrives in time. The cost is the deep defender who left, and a play-action shot over him beats it.
  • Play two-high and fit the run from depth. Two-high shells, especially the quarters family of 06-05 (a two-deep zone in which the safeties also help against the run), keep the overhang defender honest on the pass and let a safety who starts ten yards deep become the extra run defender once he sees a handoff. Against a pre-snap count, this is a defense that shows six in the box and plays like seven. Its weakness is the run itself. Unlike the rotating safety above, this one reads first (run or pass?) and only then comes down, so he arrives late, often after the back is already past the line.
  • Take away the conflict with man coverage. In man coverage, the defender over the slot isn’t torn between a gap and an area: he has a man, and he goes with him. Nobody is in conflict, so there’s nothing to read. The price is the one man coverage always pays: defenders with their backs to the ball, and a box whose count the run can attack.
  • Squeeze the clock. Hold the read defender’s decision as long as possible (a bluff, a slow-play) so the quarterback rides the mesh a beat longer and the linemen, run blocking, drift past a yard. The flag is a defensive weapon too, and a defense that has watched the tape knows which linemen climb early. The cost: a defender who is still deciding is in neither job, so against an offense that just hands off on time, the run gets a defender who arrives a step late.
  • Hit the quarterback. Every RPO exposes him for a moment with his eyes on one defender and his body turned toward the back. While he still holds the ball at the mesh he can be tackled like any ball carrier, and a rusher who arrives just as he throws may still finish the hit within a step of the release (later than that is roughing the passer).22 Pressure off the edge makes him hurry the read. The cost: the extra rusher comes from somewhere, usually the coverage, and the quick throws an RPO carries go to exactly the space he left.

Offenses answer back, as always: with motion that changes who the conflict defender is just before the snap, with RPOs that read the safety instead of the overhang, and with play-action that looks exactly like the RPO for the first second and then throws deep over the safety who has learned to come down. That conversation, how one play makes the next one work, is the subject of Constraint Theory, where the RPO appears as a constraint play: a play that punishes a defense for overplaying the offense’s base run.

TipWatch for it

On Sunday: “Is he reading you?” Every RPO has a defender being read. Find him before the snap and you’ll see the decision before the replay does.

Before the snap

  • Shotgun or pistol, back beside the quarterback, receivers spread. That’s the RPO look. Not every snap from it is an RPO, so treat it as “possible.”
  • Count the box, then count outside. Six or seven in the box? How many defenders are within seven yards of the line over the slot receivers? If the box is heavy and the slots are light, expect a throw (a bubble or now screen) the instant the ball is snapped.
  • Find the apex. Who stands halfway between the box and a slot receiver? On the single-receiver side, is a safety walked down? That’s the likely read. Say it out loud: “he’s being read.”

At the snap

  • RPO or play-action? Watch the line for half a second. On an RPO the linemen fire forward, low, and stay on their blocks at the line; nobody climbs. On play-action they make one hard run step, then their helmets pop up and they settle into pass protection, often a step behind the line. And on a true run, the uncovered guard climbs to a linebacker. Forward and staying put means RPO.
  • Where the quarterback’s head turns. On a plain handoff, he looks at the back. On a zone read, his eyes are on a defensive lineman at the line. On a post-snap RPO they go to the second level or out over a slot: his eyes are on a defender. On the live sideline shot you can usually see his helmet turn; for his actual eyes and the defender he’s watching, wait for the end-zone replay (01-06).
  • The backside receiver. On a pure run he jogs or blocks his cornerback. On an RPO he runs a full-speed slant or glance, because he might get the ball.
  • Watch the read defender’s first two steps, not the ball. Forward and inside: predict throw, behind him. Backward, widening, or eyes on a receiver: predict give.
  • Count to one. If the ball isn’t out of the quarterback’s hand by about a second, it’s probably a handoff. (Play-action’s throw comes later, two and a half seconds or more, after a full fake and a drop.)

After the play

  • On the replay, freeze the frame when the ball leaves the quarterback’s hand and look at the linemen. Is anyone more than a yard past the line without a defender in his hands? That’s the flag the umpire is looking for. Did one get thrown?
  • On the next snap, did the read defender do the opposite? Defenses adjust within a drive.

For your prediction log (01-06): on every RPO look, write the defender you think is being read and your call (G or T) from his first steps; afterward, write what happened.

Predict the play

TipPredict the play

Drill 1: count both sides. First-and-10 at midfield. The offense is in the shotgun, 11 personnel, trips to the right with the tight end attached, the back offset left. It has thrown the bubble RPO twice already today. This time the defense shows the picture below before the snap. Count the box and count outside the box on the trips side. Hand off or throw the bubble? And is there anything on the left side the quarterback should like?

Show the Python
p = Play(trips(), nickel("single_high", NB=(5.0, 9.6), SS=(6.0, 13.4), FS=(13.5, 0.0)), los=60, to_go=10)
f = draw(p, lateral=(-19.5, 20.0), window=(-7.2, 15.6), width_in=6.0)
panel_title(f.ax, "Drill 1: 1st and 10, the trips side crowded")
plt.show()
Figure 25.10: Drill 1: first-and-10, gun 11 personnel, trips right with the tight end attached. The defense has the nickel back over H and the strong safety walked down outside him, six yards deep; the free safety is alone in the deep middle.

Hand off, and the left side is the bonus.

  • The box: four linemen and two linebackers, six defenders against six blockers (five linemen and the tight end). A hat on every hat: the run is fairly blocked.
  • Outside on the trips side: H and Z against the nickel back, the cornerback and the strong safety, all within seven yards of the line. Two against three: the bubble is dead. The defense has done exactly what two thrown bubbles teach it to do.
  • The left side: to get that third defender over the trips side and keep six in the box, the defense has left X alone against an off cornerback, with the only safety in the middle of the field. A slant or glance to X is the throw the count favors, if the play has one packaged to that side (many RPOs pair a bubble on one side with a slant or glance on the other, for exactly this picture).

The general lesson: the count is never just one side. A defense that wins the numbers in one place has lost them somewhere else, and a pre-snap RPO is a machine for finding where.

TipPredict the play

Drill 2: freeze frame. Second-and-8. The offense runs the slant RPO of Figure 25.5 to the left, the inside-zone run going right. The picture below is frozen about six tenths of a second after the snap, with the ball in the back’s belly at the mesh. The overhang safety (ringed) has taken two quick steps, and his shoulders have turned square to the backfield. Does the quarterback give or throw? What happens if the cornerback had lined up on X’s inside shoulder instead?

Show the Python
dfn = only(nickel("single_high", LCB=(6.5, -16.4)), -20, 7)
p = Play(only(trips(), -20, 7), dfn, los=35, to_go=8)
for k, end in [("LT", (0.25, -4.2)), ("LG", (0.3, -2.2)), ("C", (0.35, 0.3)), ("RG", (0.35, 1.3)),
               ("RT", (0.3, 3.8)), ("Y", (0.35, 4.6))]:
    go_to(p, k, end)
go_to(p, "X", (1.4, -15.0), "route")
go_to(p, "SS", (3.6, -6.9), "path")
go_to(p, "RB", (-4.6, -0.3), "path")
p.highlight("SS", color=style.DEFENSE)
f = draw(p, lateral=(-19.0, 6.8), window=(-7.0, 14.6), width_in=5.6)
panel_title(f.ax, "Drill 2: 2nd and 8, frozen at the mesh")
read_line(f, p, (-4.8, -0.4), (3.6, -6.9))
plt.show()
Figure 25.11: Drill 2: the slant RPO, about 0.6 seconds after the snap. Players are drawn where they stood at the snap; the lines show where each has gone by the moment of the read: the line run-blocking right, the back arriving at the mesh, X two steps into his stem, and the overhang safety’s first two steps.

Throw the slant. In six tenths of a second the safety has come two yards forward and two yards inside: shoulders toward the run, eyes in the backfield. He has chosen the run, and in doing so he has left the slant’s landing spot, five yards past the line where he was standing. The quarterback pulls the ball and throws it on time, before the linemen have had a chance to drift past a yard. The cornerback, seven yards off and outside, can’t get there. (Compare Figure 25.5: this is its upper branch.)

If the cornerback had lined up inside, the slant would have been a bad throw no matter what the safety did: the corner sits on the route’s path. The quarterback would know that before the snap, and on a well-built call he’d have a second answer on that side (a quick out or a bubble) or he’d treat the play as a straight handoff. Leverage picks the live route; the read picks whether to throw it.

TipPredict the play

Drill 3: flag or no flag? A glance RPO goes for 15 yards. On the replay, frozen at the instant the ball left the quarterback’s hand, you see the picture below. The left guard blocked the 3-technique at the line, let go, and is now about two and a half yards past the line on his way to the Will. The right tackle engaged the 7-technique at the line and has driven him two yards backward, still locked on. Is there a foul in an NFL game? In a college game?

Show the Python
base = only(trips(), -18, 7)
spots = {"LT": (0.9, -4.3), "LG": (2.5, -2.9), "C": (0.3, -0.4), "RG": (0.3, 1.3), "RT": (1.8, 4.0),
         "Y": (0.0, 5.8), "QB": (-5.4, -1.2), "RB": (-3.6, 0.9), "X": (7.2, -13.2)}
dspots = {"BE": (1.9, -4.9), "BT": (1.4, -1.3), "PT": (1.9, 0.45), "PE": (2.9, 4.3),
          "WILL": (5.4, -3.8), "MIKE": (3.8, 1.8), "LCB": (11.0, -16.0), "FS": (12.6, -8.2)}
off = [q.moved(d=spots[q.key][0], w=spots[q.key][1]) if q.key in spots else q for q in base]
dfn = [q.moved(d=dspots[q.key][0], w=dspots[q.key][1]) if q.key in dspots else q
       for q in only(nickel("two_high"), -18, 7)]
p = Play(off, dfn, los=35, to_go=None)
p.highlight("LG", "RT", color=style.SERIES[6])
f = draw(p, lateral=(-17.5, 8.5), window=(-6.4, 14.2), width_in=5.6)
region(f, p, 0.0, 1.0, -17.4, 8.4, style.MUTED, alpha=0.10, lw=0.6, ls=(0, (2, 2)))
throw(f, p, (-5.4, -1.2), (11.2, -11.6), rad=-0.1)
point_to(f, p, (7.8, -0.8), (2.5, -2.9), "LG: 2½ yards past,\nhands empty", fontsize=6.6)
point_to(f, p, (7.4, 5.0), (1.8, 4.0), "RT: 2 yards past,\nstill blocking", fontsize=6.6)
say(f, p, 1.5, -12.6, "1 yard", fontsize=6.4, color=style.INK_2)
panel_title(f.ax, "Drill 3: the ball is out")
plt.show()
Figure 25.12: Drill 3: the instant of the throw. The left guard (ringed) is about two and a half yards past the line of scrimmage with no defender in his hands; the right tackle (ringed) is two yards past it, still blocking the defensive end he engaged at the line. The pass is on its way to X.

NFL: yes, on the left guard; the right tackle is fine. Illegal man downfield, five yards from the previous spot, and the 15-yard completion comes back. The guard engaged the 3-technique within a yard of the line, but he let go and kept moving forward, which the rule forbids once he’s past a yard. The right tackle is legal under the rule’s exception: he made contact at the line on his initial charge and has stayed locked on while driving his man downfield.

College: no foul. The limit is three yards beyond the neutral zone, and the guard, about two and a half yards past the line of scrimmage, is inside it. He hasn’t reached the Will yet, which matters too: had he already been blocking him that far downfield, college could call offensive pass interference instead. The right tackle is legal under either rulebook.

This is the NFL RPO’s whole problem in one frame. The guard did exactly what inside zone teaches: a first block and a climb to the backside linebacker. That’s why NFL coaches either keep the guard on his first block when the play might be a pass, or read the Will instead of climbing to him, which is the glance RPO.

Takeaways

  • An RPO blocks a run and carries a pass on the same snap, and the quarterback picks one. Unlike play-action, the run is real; unlike the option, one branch is a pass. Every RPO is a packaged play.
  • It attacks a conflict defender with a run job and a pass job, most often the apex or overhang defender, sometimes a linebacker or a safety. If he plays the run, throw behind him; if he plays the pass, hand off. The run is the default.
  • Pre-snap RPOs (the bubble RPO) are decided by counting the box against the defenders outside; post-snap RPOs (the glance, slant and stick RPOs and the pop pass) are decided by watching one defender at the mesh. The read does a blocker’s job.
  • The NFL’s illegal man downfield rule (one yard, for any forward pass, with an exception for linemen still engaged with a defender they met at the line) makes NFL RPOs quick and short and keeps NFL linemen near the line; college’s three yards past the neutral zone, only for passes that cross it, lets college RPOs run-block longer and throw farther.
  • RPOs grew out of Air Raid tags, the zone read and the college spread, and the Reid tree carried them to the NFL (the 2017 Chiefs and Eagles). In FTN’s charting they are a few percent of NFL plays, mostly handoffs, with throws that cluster at or behind the line, and about as efficient per play as anything else, with less variance.
  • Defenses answer by reading the line instead of the back (a guard who climbs means run), making the read lie (a safety called down into the vacated run fit), playing two-high and fitting from depth, playing man to remove the conflict, stretching the clock to draw flags, and hitting the quarterback. Each one gives something back: a deep defender, a step in the run fit, or a rusher’s worth of coverage.

Connections

Builds on: Option Football (the read key, the mesh and give or keep), Play-Action, Bootlegs, and Screens (the fake that an RPO doesn’t need), A First Look at Defense (the box and the apex), Zone Running (inside zone and the climb) and The Quick Game (the bubble, slant and stick).

Sets up:

See also (case studies): Andy Reid and the Kansas City Chiefs and The Eagles: Trenches, RPOs, and the Tush Push.

If you want to go deeper: Chris B. Brown’s The Art of Smart Football and his Smart Football essays on packaged plays and the RPO; the RPO film breakdowns in USA Football’s coaching blog and in college coaching clinics (the glance, bubble and slant RPOs are standard clinic topics); and, for the rules, the NFL’s official playing rules (Rule 8, Sections 3 and 5) next to the NCAA rulebook (Rule 7-3), which make the one-yard and three-yard difference concrete.

NoteGo deeper

From diagrams to tracking data. The animation in this chapter is a gridiron simulation written in NFL Big Data Bowl coordinates (simulated, BDB-format), so real tracking data drops into the same drawing code. The 2025 Big Data Bowl’s plays.csv carries a PFF run-pass-option flag (pff_runPassOption), so finding real RPOs is a filter, not a hunt. With the tracking you can measure what this chapter only describes: how far past the line each lineman is at the frame of the pass_forward event, and how long the quarterback held the mesh. The cell below shows the shape of that code (it is not run, because the course does not download the Big Data Bowl files).

Show the Python (real tracking data; not run)
import pandas as pd
from gridiron import bdb, show_animation

trk = bdb.load_tracking("data/bdb/tracking_week_*.csv")
plays = pd.read_csv("data/bdb/plays.csv")
rpos = plays[plays["pff_runPassOption"] == 1]                    # PFF's RPO flag (BDB 2025)
g, pl = rpos.iloc[0][["gameId", "playId"]]
one = bdb.prepare(trk, plays, game_id=g, play_id=pl)
show_animation(one, times=[0.0, 0.6, 1.0, 1.8])

# How far past the line is each offensive lineman when the ball is thrown?
los = one.loc[(one["frameType"] == "SNAP") & (one["side"] == "ball"), "x"].iloc[0]
throw_f = one.loc[one["event"] == "pass_forward", "frameId"].min()
linemen = one[(one["frameId"] == throw_f) & one["position"].isin(["T", "G", "C"])]
print((linemen["x"] - los).round(1).tolist())                  # yards past the line

  1. On the glance RPO’s mechanics (a play that “incorporates a skinny post route into the RPO system”; if the read linebacker “drops into a zone, this is a hand-off,” and if he “commits to the run,” the receiver “replaces the linebacker with his route”), see “Purdue Football: Penn State on Film - RPO Glance Route,” Hammer and Rails (Aug. 30, 2022): https://www.hammerandrails.com/2022/8/30/23329647/purdue-football-penn-state-on-film-rpo-glance-route; and Inside the Pylon / Scouting Academy, “ITP Glossary: Post-Snap RPO Reads”: https://scoutingacademy.com/itp-glossary-post-snap-rpo-reads/. Which defender is read varies by team and coverage; reading the backside linebacker, as in this chapter’s diagram, is one common version.↩︎

  2. NFL, 2026 Official Playing Rules of the National Football League, Rule 8, Section 3, Article 1 (“Ineligible Player Downfield”: “On a scrimmage play during which a legal forward pass is thrown, it is a foul if the entire body of an ineligible offensive player … is more than one yard beyond the line of scrimmage before the pass has been thrown”; Item 1, legally downfield after “initiating contact with an opponent within one yard of the line of scrimmage during his initial charge,” including Item 1(b): after breaking legal contact more than a yard beyond the line, “he remains stationary, moves laterally, or moves toward his own goal line until a forward pass is thrown”; Item 2, illegally downfield; penalty “Loss of five yards from the previous spot”): https://operations.nfl.com/rules-officiating/2026-nfl-rulebook. The penalty summary lists the foul as “Ineligible player downfield on pass … 8-3-1.”↩︎

  3. NCAA Football Rules (2026 edition), Rule 7-3-10: “No originally ineligible receiver shall be or have been more than three yards beyond the neutral zone until a passer throws a legal forward pass that crosses the neutral zone. A player is in violation of this rule if any part of their body is beyond the three-yard limit”; penalty five yards from the previous spot. Rule 7-3-8-b: offensive pass interference is “contact by a Team A player beyond the neutral zone that interferes with a Team B player during a legal forward pass play in which the forward pass crosses the neutral zone,” except when an ineligible player “immediately charges and contacts an opponent at a point not more than one yard beyond the neutral zone and maintains the contact for no more than three yards beyond the neutral zone” (in 2026 its penalty became 10 yards). Text as reproduced in the 2026 NCAA rules and interpretations study compilation posted by the Amarillo Football Officials Association: https://www.amarefs.org/wp-content/uploads/2026/07/RB-Combined-2026-1.pdf; Rule 7-3-10 also at the San Diego County Football Officials Association: https://www.sdcfoa.org/ncaa/rule-7-snapping-and-passing-the-ball/college-7-3-10.↩︎

  4. SI Wire, “NCAA tables proposed ineligible downfield rule” (Mar. 6, 2015), on the Playing Rules Oversight Panel postponing a move from three yards to one: https://www.si.com/college/2015/03/06/ncaa-ineligible-downfield-rule; FootballScoop, “The ineligible receiver downfield rule has been tabled by the NCAA”: https://www.footballscoop.com/2025/03/04/ineligible-receiver-downfield-rule-tabled-ncaa. The 2026 NCAA rule still reads three yards (Rule 7-3-10; see the previous note).↩︎

  5. NFL, 2026 Official Playing Rules, Rule 8, Section 5, Article 1 (pass interference “can only occur when a forward pass is thrown from behind the line of scrimmage, regardless of whether the pass is legal or illegal, or whether it crosses the line”) and Article 4(a) (offensive pass interference includes “blocking more than one yard beyond the line of scrimmage by an offensive player clearly prior to a pass being thrown”): https://operations.nfl.com/rules-officiating/2026-nfl-rulebook.↩︎

  6. Darren Hartwell, “Is Bill Belichick sending a message to NFL refs with this RPO comment?,” NBC Sports Boston (Dec. 6, 2019): “The only problem (with) the RPO play in general is just the offensive pass interference: blocking downfield conflicting with the pass. … It’s a tough call for the officials”: https://www.nbcsportsboston.com/nfl/new-england-patriots/is-bill-belichick-sending-a-message-to-nfl-refs-with-this-rpo-comment/379873/.↩︎

  7. Dennis Dodd, “How the RPO has brought the NFL and college football together in an offensive revolution,” CBS Sports (Nov. 8, 2018), on Rich Rodriguez as “one of the founding fathers of the zone read offense that birthed RPOs” and Mike Leach bringing his Air Raid offense to Oklahoma in 1999: https://www.cbssports.com/college-football/news/how-the-rpo-has-brought-the-nfl-and-college-football-together-in-an-offensive-revolution; Mike Calabrese, “EA Sports College Football 25: RPOs & Other Offensive Innovations,” Action Network (June 2024), on Mumme’s Kentucky giving Tim Couch “bubble screens or handoffs to the running back” and Rodriguez at Tulane and Clemson balancing “slants, bubble screens and short hitches” with read options: https://www.actionnetwork.com/ncaaf/ea-sports-college-football-25-rpos-offensive-innovations.↩︎

  8. Keith Farner, “WATCH: Urban Meyer takes credit for inventing the RPO, explains how it happened,” Saturday Down South (Sept. 3, 2019): https://www.saturdaydownsouth.com/sec-football/watch-urban-meyer-takes-credit-for-inventing-the-rpo-explains-how-it-happened/ (“it was a missed assignment by a wide receiver”). Meyer coached Utah in 2003 and 2004.↩︎

  9. Jared Dubin, “Here’s Brett Favre explaining how he basically invented RPOs because he was bored,” CBS Sports (June 11, 2018), from Favre’s appearance on Jon Gruden’s QB Camp: https://www.cbssports.com/nfl/news/heres-brett-favre-explaining-how-he-basically-invented-rpos-because-he-was-bored.↩︎

  10. Marc Sessler, “Jim Haslett readies for Eagles attack with Oregon tape,” NFL.com (2013), quoting Haslett (“I’ve watched 23, 24 Oregon films”) and describing Kelly’s packaged plays, which let the quarterback throw a screen to a single-covered outside receiver or hand off on an inside run: https://www.nfl.com/news/jim-haslett-readies-for-eagles-attack-with-oregon-tape-0ap1000000239150.↩︎

  11. Dennis Dodd, “What’s next for Gus Malzahn and what’s ahead for Auburn after Tigers fire embattled coach,” CBS Sports (Dec. 13, 2020) (“The man basically refined and perfected the modern run-pass option”; “It was that still-emerging RPO game that helped beat Bama in 2013’s Kick Six game”): https://www.cbssports.com/college-football/news/whats-next-for-gus-malzahn-and-whats-ahead-for-auburn-after-tigers-fire-embattled-coach.↩︎

  12. Gregg Rosenthal, “Mike Shanahan implements Baylor plays to help RG3,” NFL.com (Sept. 12, 2012), quoting Shanahan and citing Chris B. Brown’s Grantland analysis of “packaged plays”: https://www.nfl.com/news/mike-shanahan-implements-baylor-plays-to-help-rg3-0ap1000000061442.↩︎

  13. Anthony Treash, “The RPO and the Andy Reid coaching tree,” PFF (May 17, 2019): https://www.pff.com/news/pro-the-rpo-and-the-andy-reid-coaching-tree (league RPO rate from “4% of plays in 2016 to over 8% of plays last year”; the Chiefs led in 2017 at 17.1% and 2018 at 25.0%; the Eagles ranked eighth in 2016, second in 2017 and third in 2018; Pederson “served on Reid’s staff in Kansas City from 2013 to 2015”; the Bears went from 2.7% in 2017 to 19.2% in 2018).↩︎

  14. Daire Carragher, “Analyzing the effectiveness of RPOs over the last five NFL seasons,” PFF (June 25, 2026): https://www.pff.com/news/nfl-analyzing-the-effectiveness-of-rpos-over-the-last-five-nfl-seasons (league RPO rate by season: 3.84% in 2016, 6.14% in 2017, 7.85% in 2018, 7.63% in 2019, 8.66% in 2020, 9.86% in 2021, 9.97% in 2022, 10.52% in 2023, 8.96% in 2024, 7.95% in 2025).↩︎

  15. Tim McManus, “Eagles pull from Chip Kelly playbook to awaken 2013 Nick Foles,” ESPN (Jan. 26, 2018): https://www.espn.com/blog/philadelphia-eagles/post/_/id/24097/eagles-pull-from-chip-kelly-playbook-to-awaken-2013-nick-foles (Reich’s quote; the bye-week film study of Foles’s 2013 season under Kelly, “27 touchdowns to two interceptions”); Jack Browne, “Blueprint for an upset: How the Eagles can shock the Patriots,” theScore (before Super Bowl LII, 2018), citing PFF for Foles’s 93.9% completion rate on RPOs “this season”: https://www.thescore.com/nfl/news/1474361; “Eagles quarterback Nick Foles wins Super Bowl LII MVP,” NFL.com (Feb. 4, 2018): https://www.nfl.com/news/eagles-quarterback-nick-foles-wins-super-bowl-lii-mvp-0ap3000000914463; “Super Bowl LII,” Wikipedia: https://en.wikipedia.org/wiki/Super_Bowl_LII (U.S. Bank Stadium, Minneapolis). Wentz’s injury: Associated Press, “AP sources: Doctors believe Eagles’ Carson Wentz tore ACL, out for year,” via Boston.com (Dec. 11, 2017), on the Dec. 10 win at the Los Angeles Rams: https://www.boston.com/sports/nfl/2017/12/11/ap-sources-doctors-believe-eagles-carson-wentz-tore-acl-out-for-year. Game lines (NFC Championship, Jan. 21, 2018: Foles 26 of 33, 352 yards, 3 TD, Eagles 38–7; Super Bowl LII: 373 yards, 3 TD passes, a TD catch from Trey Burton, Eagles 41–33): nflverse play-by-play, this book’s check.↩︎

  16. Same PFF article (Carragher, 2026): Mahomes led all quarterbacks in RPO pass attempts since 2020 (391), ahead of Aaron Rodgers (287) and Josh Allen (268).↩︎

  17. FTN charting via nflverse (load_ftn_charting joined to load_pbp), 2025 regular season, Kansas City plays flagged is_rpo: 150 plays (88 runs, 62 passes), 98.7% from the shotgun, 56.7% on first down; median air yards on the passes −2; most targets Rashee Rice 17, Xavier Worthy 11. This book’s calculation.↩︎

  18. FTN charting via nflverse, this chapter’s calculation (see the data figure): highest team RPO share, 2022 Philadelphia; 2023 Indianapolis. Steichen was the Eagles’ offensive coordinator in 2021 and 2022 and was hired as the Colts’ head coach in February 2023: “Shane Steichen,” Wikipedia: https://en.wikipedia.org/wiki/Shane_Steichen.↩︎

  19. nflverse play-by-play for game 2022_22_KC_PHI (Super Bowl LVII, Feb. 12, 2023), joined to FTN charting: play 192 (1st quarter, 12:31, 1st-and-10, “J.Hurts pass short right to 88-D.Goedert … for 13 yards”), play 216 (12:02, “No Huddle, Shotgun … pass short right to 6-D.Smith … for 23 yards”) and play 995 (2nd quarter, 15:00, “pass deep left to 11-A.Brown for 45 yards, TOUCHDOWN”) all have is_rpo and is_play_action true. This book’s calculation.↩︎

  20. This book’s calculation from nflverse play-by-play (load_pbp) joined on game and play ID to FTN charting (load_ftn_charting, flag is_rpo), regular seasons 2022–2025, plays with play_type pass or run and a down (so scrambles and sacks on RPO looks are included; spikes and kneel-downs are not). Air yards: pbp air_yards on RPO passes. Box counts: FTN n_defense_box. Ineligible-downfield fouls: pbp penalty_type == "Ineligible Downfield Pass" on any regular-season play, joined to FTN’s flag. EPA and success as defined in Expected Points, EPA and Success Rate.↩︎

  21. Same PFF article (Carragher, 2026): 2021–2025, RPOs 4.53 yards per play and a 52.3% successful-play rate, non-RPO plays 5.43 and 48.1%. PFF’s success definition is its own and differs from the EPA-based success rate used elsewhere in this book.↩︎

  22. NFL, 2026 Official Playing Rules, Rule 12, Section 2, Article 11(a): “once a pass has been released by a passer, a rushing defender may make direct contact with the passer only up through the rusher’s first step after such release”: https://operations.nfl.com/rules-officiating/2026-nfl-rulebook.↩︎