Skip to content

Plan Rewrites

DocETL lifts every pipeline into a typed logical plan before running it and applies a small set of equivalence-preserving rewrites — reorderings that cut LLM cost without changing your output. Unlike MOAR, which searches for accuracy/cost trade-offs offline, plan rewrites are free wins applied automatically at the start of every run.

What the rules do

Selection pushdown moves a filter below an LLM map that doesn't produce anything the filter reads, so the map only processes rows the filter keeps:

map(1000 rows) → filter(keeps 200)        # before: 1000 LLM calls
filter(keeps 200) → map(200 rows)         # after:   200 LLM calls

This fires only when DocETL can prove the swap is safe: the filter's predicate must not reference any field the map writes (including drop_keys), the map must be one-output-per-row with no reordering, and neither op's output may be shared by another step. The analysis is fail-closed — if a prompt or code predicate reads fields in a way DocETL can't statically enumerate ({{ input.items() }}, dynamic keys, multi-arg callables), the rewrite simply doesn't fire.

Limit pushdown moves a positional head (sample with method: first) below one-to-one ops, so upstream LLM calls run only on the rows that survive the head. One caveat: if an LLM call exhausts its retries, the row is silently dropped today, and a head that was pushed below such an op can then select a slightly different row set. This only matters in runs that are already losing rows to that failure mode; disable the rule (below) if exact head semantics matter more to you than the cost saving.

Seeing what fired

frame.explain(optimized=True)
# -- applied selection_pushdown: pushed keep (filter) below summarize (map), ...
# keep [filter · selection · llm]
#   summarize [map · 1:1 · llm] +{summary: string}
#     scan(docs)

frame.plan() returns the typed plan for programmatic inspection. At run time, every applied rewrite is logged as Plan rewrite — ....

Turning rewrites off (or down)

Rewrites are on by default. Control them per pipeline or globally:

plan_rewrites: false                      # off entirely
# or select rules by name:
plan_rewrites: ["selection_pushdown"]     # no limit pushdown
import docetl
docetl.plan_rewrites = False              # all in-process runs

Misspelled rule names raise an error rather than silently disabling anything. The CLI runs in its own process, so for docetl run use the YAML key.

Checkpoints

Checkpoint hashes are computed over the rewritten pipeline. The first run after a rewrite starts firing (or after toggling plan_rewrites) re-executes instead of reusing old checkpoints — a one-time cost, never a correctness issue.

MOAR integration

MOAR candidates are validated statically before any execution cost is paid (malformed directive output is rejected immediately), and each saved candidate is canonicalized with the same rewrite rules — honoring your plan_rewrites setting. Set the DOCETL_MOAR_PLAN_SUMMARY environment variable to include the typed plan in the rewrite agent's context.