Build and run LLM-powered data processing pipelines with DocETL. Use when users say "docetl", want to analyze unstructured data, process documents, extract information, or run ETL tasks on text. Helps with data collection, pipeline creation, execution, and optimization.
Build and run LLM-powered data processing pipelines with DocETL. Use when users say "docetl", want to analyze unstructured data, process documents, extract information, or run ETL tasks on text. Helps with data collection, pipeline creation, execution, and optimization.
DocETL Pipeline Development
DocETL is a system for creating LLM-powered data processing pipelines. This skill helps you build end-to-end pipelines: from data preparation to execution and optimization.
Workflow Overview: Iterative Data Analysis
Work like a data analyst: write → run → inspect → iterate. Never write all scripts at once and run them all at once. Each phase should be completed and validated before moving to the next.
Phase 1: Data Collection
Write data collection script
Run it immediately (with user permission)
Inspect the dataset - show the user:
Total document count
Keys/fields in each document
Sample documents (first 3-5)
Length distribution (avg chars, min/max)
Any other relevant statistics
Iterate if needed (e.g., collect more data, fix parsing issues)
Phase 2: Pipeline Development
Read sample documents to understand format
Write pipeline YAML with sample: 10-20 for testing
Run the test pipeline
Inspect intermediate results - show the user:
Extraction quality on samples
Domain/category distributions
Any validation failures
Iterate on prompts/schema based on results
Remove sample parameter and run full pipeline
Show final results - distributions, trends, key insights
Phase 3: Visualization & Presentation
Write visualization script based on actual output structure
Run and show the report to the user
Iterate on charts/tables if needed
Visualization Aesthetics:
Clean and minimalist - no clutter, generous whitespace
Warm and elegant color theme - 1-2 accent colors max
Subtle borders - not too rounded (border-radius: 8-10px max)
Sans-serif fonts - system fonts like -apple-system, Segoe UI, Roboto
"Created by DocETL" - add subtitle after the main title
Mix of charts and tables - charts for distributions, tables for detailed summaries
Light background - off-white (#f5f5f5) with white cards for content
If user needs to collect data, write a Python script:
import json
# Collect/transform data
documents = []
for source in sources:
documents.append({
"id": source.id,
"text": source.content, # DO NOT truncate text# Add relevant fields
})
# Save as DocETL datasetwithopen("dataset.json", "w") as f:
json.dump(documents, f, indent=2)
Important: Never truncate document text in collection scripts. DocETL operations like split handle long documents properly. Truncation loses information.
After Running Data Collection
Always run the collection script and inspect results before proceeding. Show the user:
import json
data = json.load(open("dataset.json"))
print(f"Total documents: {len(data)}")
print(f"Keys: {list(data[0].keys())}")
print(f"Avg length: {sum(len(str(d)) for d in data) // len(data)} chars")
# Show sampleprint("\nSample document:")
print(json.dumps(data[0], indent=2)[:500])
Only proceed to pipeline development once the data looks correct.
Step 2: Read and Understand the Data
CRITICAL: Before writing any prompts, READ the actual input data to understand:
The structure and format of documents
The vocabulary and terminology used
What information is present vs. absent
Edge cases and variations
import json
withopen("dataset.json") as f:
data = json.load(f)
# Examine several examplesfor doc in data[:5]:
print(doc)
This understanding is essential for writing specific, effective prompts.
Step 3: Pipeline Structure
DocETL supports two equivalent approaches. Use whichever the user prefers:
Option A: YAML (low-code)
Create a YAML file with this structure:
default_model:gpt-5-nanosystem_prompt:dataset_description:<describethedatabasedonwhatyouobserved>persona:<rolefortheLLMtoadopt>datasets:input_data:type:filepath:"dataset.json"# or dataset.csvoperations:-name:<operation_name>type:<operation_type>prompt:|
<Detailed, specific prompt based on the actual data>
output:schema:<field_name>:<type>pipeline:steps:-name:processinput:input_dataoperations:-<operation_name>output:type:filepath:"output.json"intermediate_dir:"intermediates"# ALWAYS set this for debugging
All operation parameters are the same between YAML and Python — just pass them as keyword arguments (e.g., validate=["len(output['items']) >= 1"], fold_prompt="...", fold_batch_size=100).
Tool-equipped agents (Python API only)
Use agent=docetl.Agent(...) on .map(), .filter(), or .reduce() when an operation needs tools before returning structured output:
Agents are Python-only; do not put agent configs in YAML and do not export them with .to_yaml() / .to_python().
The operation model= still selects the model. Python tools wrapped with @docetl.tool are the most provider-portable path through LiteLLM-compatible models.
OpenAI-hosted tools (WebSearchTool, hosted ShellTool, docetl.tools.Sandbox.create(...)) require an OpenAI hosted-tool path. docetl.tools.Sandbox.create(...) creates a persistent OpenAI hosted container; sandbox.bash() returns a shell tool bound to that container.
Specialist subagents can be exposed as manager-agent tools with specialist.as_tool(name=..., description=...).
default_model: Use gpt-5-nano or gpt-5-mini for extraction/map operations
intermediate_dir: Always set to log intermediate results
system_prompt: Describe the data based on what you actually observed
Model Selection by Operation Type
Operation Type
Recommended Model
Rationale
Map (extraction)
gpt-5-nano or gpt-5-mini
High volume, simple per-doc tasks
Filter
gpt-5-nano
Simple yes/no decisions
Reduce (summarization)
gpt-4.1 or gpt-5.1
Complex synthesis across many docs
Resolve (deduplication)
gpt-5-nano or gpt-5-mini
Simple pairwise comparisons
Use cheaper models for high-volume extraction, and more capable models for synthesis/summarization where quality matters most.
Step 4: Writing Effective Prompts
Prompts must be specific to the data, not generic. After reading the input data:
Bad (Generic) Prompt
prompt:|
Extract key information from this document.
{{ input.text }}
Good (Specific) Prompt
prompt:|
You are analyzing a medical transcript from a doctor-patient visit.
The transcript follows this format:-Doctorstatementsareprefixedwith"DR:"-Patientstatementsareprefixedwith"PT:"-Timestampsappearinbracketslike [00:05:23]
Fromthefollowingtranscript,extract:1.Allmedicationsmentioned(brandnamesorgeneric)2.Dosagesifspecified3.Patient-reportedsideeffectsorconcernsTranscript:
{{ input.transcript }}
Bethorough-patientsoftenmentionmedicationnamesinformally.Ifamedicationisunclear,includeitwithanote.
Prompt Writing Guidelines
Describe the data format you observed
Be specific about what to extract - list exact fields
Mention edge cases you noticed in the data
Provide examples if the task is ambiguous
Set expectations for handling missing/unclear information
Step 5: Choosing Operations
Many tasks only need a single map operation. Use good judgement:
Task
Recommended Approach
Extract info from each doc
Single map
Multiple extractions
Multiple map operations chained
Extract then summarize
map → reduce
Filter then process
filter → map
Split long docs
split → map → reduce
Deduplicate entities
map → unnest → resolve
Operation Reference
Map Operation
Applies an LLM transformation to each document independently.
-name:extract_infotype:mapprompt:|
Analyze this document:
{{ input.text }}
Extractthemaintopicand3keypoints.output:schema:topic:stringkey_points:list[string]model:gpt-5-nano# optional, uses default_model if not setskip_on_error:true# recommended for large-scale runsvalidate:# optional-len(output["key_points"])==3num_retries_on_validate_failure:2# optional
Key parameters:
prompt: Jinja2 template, use {{ input.field }} to reference fields
output.schema: Define output structure
skip_on_error: Set true to continue on LLM errors (recommended at scale)
validate: Python expressions to validate output
sample: Process only N documents (for testing)
limit: Stop after producing N outputs
Filter Operation
Keeps or removes documents based on LLM criteria. Output schema must have exactly one boolean field.
Always include fold_prompt and fold_batch_size for reduce operations. This handles cases where the group is too large to fit in context.
-name:summarize_by_categorytype:reducereduce_key:category# use "_all" to aggregate everythingskip_on_error:trueprompt:|
Summarize these {{ inputs | length }} items for category "{{ inputs[0].category }}":
{%foritemininputs%}
- {{ item.title }}: {{ item.description }}
{%endfor%}
Providea2-3sentencesummaryofthekeythemes.fold_prompt:|
You have a summary based on previous items, and new items to incorporate.
Previoussummary(basedon {{ output.item_count }} items):
{{ output.summary }}
Newitems({{inputs|length}}more):
{%foritemininputs%}
- {{ item.title }}: {{ item.description }}
{%endfor%}
WriteaNEWsummarythatcoversALLitems(previous+new).IMPORTANT:Outputaclean,standalonesummaryasifdescribingtheentiredataset.DoNOTmention"updated","added","new items",orreferencetheincrementalprocess.fold_batch_size:100output:schema:summary:stringitem_count:intvalidate:-len(output["summary"].strip())>0num_retries_on_validate_failure:2
Critical: Writing Good Fold Prompts
The fold_prompt is called repeatedly as batches are processed. Its output must:
Reflect ALL data seen so far, not just the latest batch
Be a clean, standalone output - no "updated X" or "added Y items" language
Match the same schema as the initial prompt output
Bad fold_prompt output: "Added 50 new projects. The updated summary now includes..."
Good fold_prompt output: "Developers are building privacy-focused tools and local-first apps..."
Estimating fold_batch_size:
Use 100+ for most cases - larger batches = fewer LLM calls = lower cost
For very long documents, reduce to 50-75
For short documents (tweets, titles), can use 150-200
Models like gpt-4o-mini have 128k context, so batch size is rarely the bottleneck
Key parameters:
reduce_key: Field to group by (or list of fields, or _all)
fold_prompt: Template for incrementally adding items to existing output (required)
fold_batch_size: Number of items per fold iteration (required, use 100+)
associative: Set to false if order matters
Split Operation
Divides long text into smaller chunks. No LLM call.
-name:split_documenttype:splitsplit_key:contentmethod:token_count# or "delimiter"method_kwargs:num_tokens:500model:gpt-5-nano
Output adds:
{split_key}_chunk: The chunk content
{op_name}_id: Original document ID
{op_name}_chunk_num: Chunk number
Unnest Operation
Flattens list fields into separate rows. No LLM call.
-name:unnest_itemstype:unnestunnest_key:items# field containing the listkeep_empty:false# optional
Example: If a document has items: ["a", "b", "c"], unnest creates 3 documents, each with items: "a", items: "b", items: "c".
Resolve Operation
Deduplicates and canonicalizes entities. Uses pairwise comparison.
-name:dedupe_namestype:resolveoptimize:true# let optimizer find blocking rulesskip_on_error:truecomparison_prompt:|
Are these the same person?
Person 1: {{ input1.name }} ({{input1.email}})Person 2: {{ input2.name }} ({{input2.email}})Respondtrueorfalse.resolution_prompt:|
Standardize this person's name:
{%forentryininputs%}
- {{ entry.name }}
{%endfor%}
Returnthecanonicalname.output:schema:name:string
Important: Set optimize: true and run docetl build to generate efficient blocking rules. Without blocking, this is O(n²).
Code Operations
Deterministic Python transformations without LLM calls.
-name:find_conflictstype:mapretriever:facts_indexprompt:|
Check if this fact conflicts with any retrieved facts:
Current fact: {{ input.fact }} (from {{ input.source }})Related facts from other articles:
{{ retrieval_context }}
Returnwhetherthere'sagenuineconflict.output:schema:has_conflict:boolean
Python API — create a docetl.Retriever object and pass it to operations:
The retriever parameter is available on .map(), .filter(), .reduce(), and .extract().
In the Python API, pass the data to index directly with data= (a file path or list of dicts — use this for external knowledge bases), or reference an existing pipeline dataset with dataset=: the frame's own input (file basename, or from_list's name=, default "data") or a previous step's output (step_<operation_name>).
Key points:
{{ retrieval_context }} is injected into prompts automatically
Index is built on first use (when build_index: if_missing)
Supports full-text (fts), vector (embedding), or hybrid search
Use save_retriever_output: true to debug what was retrieved
Can index intermediate outputs: Retriever can index the output of a previous pipeline step, enabling patterns like "extract facts → index facts → retrieve similar facts for each"
Documentation Reference
For detailed parameters, advanced features, and more examples, read the docs:
Always test on a sample first, then run full pipeline.
Test Run (Required)
Add sample: 10-20 to your first operation, then run:
YAML:
docetl run pipeline.yaml
Python:
# Add sample=10 to the first operation for testing
results = (
docetl.read_json("dataset.json")
.map(prompt="...", output={"schema": {"field": "type"}}, sample=10)
.collect()
)
Inspect the test results before proceeding:
import json
from collections import Counter
# Load intermediate results
data = json.load(open("intermediates/step_name/operation_name.json"))
print(f"Processed: {len(data)} docs")
# Check distributionsif"domain"in data[0]:
print("Domain distribution:")
for k, v in Counter(d["domain"] for d in data).most_common():
print(f" {k}: {v}")
# Show sample outputsprint("\nSample output:")
print(json.dumps(data[0], indent=2))
Full Run
Once test results look good:
Remove the sample parameter from the pipeline
Ask user for permission (estimate cost based on test run)
Run full pipeline
Show final results - distributions, key insights, trends
Options:
--max_threads N - Control parallelism
Check intermediate results in the intermediate_dir folder to debug each step.
Step 8: Optimization (Optional)
Use MOAR optimizer to find the Pareto frontier of cost vs. accuracy tradeoffs. MOAR experiments with different pipeline rewrites and models to find optimal configurations.
YAML approach — add optimizer_config to the pipeline:
optimizer_config:type:moarsave_dir:./optimization_resultsavailable_models:-gpt-5-nano-gpt-4o-mini-gpt-4oevaluation_file:evaluate.py# User must providemetric_key:scoremax_iterations:20model:gpt-5-nano
MOAR will produce multiple pipeline variants on the Pareto frontier - user can choose based on their cost/accuracy preferences.
Output Schemas
Keep schemas minimal and simple unless the user explicitly requests more fields. Default to 1-3 output fields per operation. Only add more fields if the user specifically asks for them.
Nesting limit: Maximum 2 levels deep (e.g., list[{field: str}] is allowed, but no deeper).
# Good - minimal, focused on the core taskoutput:schema:summary:string# Good - a few fields when task requires itoutput:schema:topic:stringkeywords:list[string]# Acceptable - 2 levels of nesting (list of objects)output:schema:items:"list[{name: str, value: int}]"# Bad - too many fields (unless user explicitly requested all of these)output:schema:conflicts_found:boolnum_conflicts:intconflicts:"list[{claim_a: str, source_a: str, claim_b: str, source_b: str}]"analysis_summary:str# Bad - more than 2 levels of nesting (not supported)output:schema:data:"list[{nested: {too: {deep: str}}}]"
Guidelines:
Start with the minimum fields needed to answer the user's question
prompt:|
Summarize these {{ inputs | length }} items:
{% for item in inputs %}
- {{ item.summary }}
{% endfor %}
Troubleshooting
Pipeline won't run
Check .env has correct API keys
Verify dataset file exists and is valid JSON/CSV
Check YAML syntax
Bad outputs
Read more input data examples to improve prompt specificity
Add validate rules with retries
Simplify output schema
Add concrete examples to prompt
High costs
Use gpt-5-nano or gpt-4o-mini
Add sample: 10 to test on subset first
Run MOAR optimizer to find cost-efficient rewrites
Check intermediate results
Look in intermediate_dir folder to debug each step.
Quick Reference
# Run pipeline
docetl run pipeline.yaml
# Run with more parallelism
docetl run pipeline.yaml --max_threads 16
# Optimize pipeline (cost/accuracy tradeoff)
docetl build pipeline.yaml --optimizer moar
# Clear LLM cache
docetl clear-cache
# Check version
docetl version