| name | automated-orchestration |
| description | Use when building multi-stage analysis pipelines that need checkpoint/resume, when long-running jobs fail mid-execution and lose progress, when processing large data archives that exceed memory or rate limits, or when analysis depth should adapt to data volume |
Automated Orchestration with Checkpoints
Overview
Coordinate end-to-end analytical pipelines with checkpoint/resume capabilities and activity-based depth adjustment. The core principle: every stage that can fail must be resumable, and every pipeline must adapt its depth to the data it encounters.
When to Use
- Pipeline processes multiple stages sequentially and failures lose hours of work
- Large data corpus requires chunked processing with rate-limit awareness
- Analysis depth should scale with data volume (shallow scan vs. deep dive)
- Long-running jobs need graceful interruption and restart
- Multiple data sources must be combined across stages with partial-failure tolerance
When NOT to use:
- Single-shot scripts under 5 minutes with no intermediate state
- Pipelines where full re-execution is cheap and acceptable
- Real-time streaming (use event-driven patterns instead)
Core Pattern
WITHOUT orchestration: WITH orchestration:
fetch_all_data() --> Stage 1: fetch [checkpoint] -->
process_everything() --> Stage 2: parse [checkpoint] -->
analyze_all() --> Stage 3: enrich [checkpoint] -->
generate_report() Stage 4: report [checkpoint]
Fails at analyze? Fails at enrich?
Start over from scratch. Resume from Stage 3 checkpoint.
Quick Reference
| Concept | Pattern | Anti-pattern |
|---|
| Checkpointing | JSON/pickle per stage, atomic writes | No checkpoints on jobs > 5 min |
| Idempotency | Delete-then-write, upsert by key | Append-only without dedup |
| Depth adjustment | Scale analysis tiers to item count | Hardcoded depth for all inputs |
| Rate limits | Exponential backoff with jitter | Retry loops without delay |
| Progress | Structured log per stage with counts | Silent processing, no visibility |
| Failure handling | Isolate stages, continue on partial | Abort entire pipeline on one error |
Pipeline Architecture
1. Define stages as idempotent units
Each stage reads an input artifact, produces an output artifact, and writes a checkpoint on success. Re-running a completed stage is a no-op.
import json, os, time, hashlib
from pathlib import Path
class PipelineStage:
"""Base for an idempotent, checkpointed pipeline stage."""
def __init__(self, name, checkpoint_dir="checkpoints"):
self.name = name
self.checkpoint_dir = Path(checkpoint_dir)
self.checkpoint_dir.mkdir(parents=True, exist_ok=True)
@property
def checkpoint_path(self):
return self.checkpoint_dir / f"{self.name}.json"
def is_complete(self):
"""Check if this stage already finished successfully."""
if not self.checkpoint_path.exists():
return False
cp = json.loads(self.checkpoint_path.read_text())
return cp.get("status") == "complete"
def load_checkpoint(self):
"""Load partial progress for resume."""
if self.checkpoint_path.exists():
return json.loads(self.checkpoint_path.read_text())
return {"status": "not_started", "processed": [], "errors": []}
def save_checkpoint(self, state):
"""Atomic checkpoint write: write tmp then rename."""
tmp = self.checkpoint_path.with_suffix(".tmp")
tmp.write_text(json.dumps(state, indent=2, default=str))
tmp.rename(self.checkpoint_path)
def run(self, input_data):
"""Execute stage with checkpoint/resume logic."""
if self.is_complete():
print(f"[{self.name}] Already complete, skipping.")
return self._load_output()
state = self.load_checkpoint()
already_done = set(state.get("processed", []))
items = [i for i in input_data if self._item_key(i) not in already_done]
print(f"[{self.name}] Processing {len(items)} items "
f"({len(already_done)} already done)")
for i, item in enumerate(items):
try:
result = self.process_item(item)
state["processed"].append(self._item_key(item))
if (i + 1) % self.checkpoint_interval == 0:
self.save_checkpoint(state)
except Exception as e:
state["errors"].append({
"item": self._item_key(item),
"error": str(e), "time": time.time()
})
self.save_checkpoint(state)
continue
state["status"] = "complete"
state["completed_at"] = time.time()
self.save_checkpoint(state)
return self._load_output()
def process_item(self, item):
raise NotImplementedError
def _item_key(self, item):
"""Unique key for deduplication."""
return hashlib.md5(json.dumps(item, sort_keys=True).encode()).hexdigest()
def _load_output(self):
"""Load this stage's output artifact."""
raise NotImplementedError
checkpoint_interval = 50
2. Orchestrate stages with a pipeline runner
class Pipeline:
"""Run stages in sequence with checkpoint awareness."""
def __init__(self, stages, checkpoint_dir="checkpoints"):
self.stages = stages
self.checkpoint_dir = Path(checkpoint_dir)
self.log = []
def run(self, initial_input):
data = initial_input
for stage in self.stages:
t0 = time.time()
try:
data = stage.run(data)
elapsed = time.time() - t0
self.log.append({
"stage": stage.name, "status": "complete",
"elapsed_s": round(elapsed, 1),
"items": len(stage.load_checkpoint().get("processed", [])),
"errors": len(stage.load_checkpoint().get("errors", []))
})
except KeyboardInterrupt:
self.log.append({"stage": stage.name, "status": "interrupted"})
print(f"\n[pipeline] Interrupted at {stage.name}. "
f"Re-run to resume from this stage.")
break
except Exception as e:
self.log.append({
"stage": stage.name, "status": "failed", "error": str(e)
})
print(f"[pipeline] Stage {stage.name} failed: {e}")
break
return data
def write_report(self, path="docs/analysis/03-automated-orchestration.md"):
"""Write pipeline status report."""
report = self._build_report()
Path(path).parent.mkdir(parents=True, exist_ok=True)
Path(path).write_text(report)
def _build_report(self):
lines = ["# Automated Orchestration Report\n"]
lines.append(f"Generated: {time.strftime('%Y-%m-%d %H:%M:%S')}\n")
lines.append("## Pipeline Status\n")
lines.append("| Stage | Status | Items | Errors | Time (s) |")
lines.append("|-------|--------|-------|--------|----------|")
for entry in self.log:
lines.append(
f"| {entry.get('stage','')} "
f"| {entry.get('status','')} "
f"| {entry.get('items','-')} "
f"| {entry.get('errors','-')} "
f"| {entry.get('elapsed_s','-')} |"
)
failures = [e for e in self.log if e["status"] != "complete"]
if failures:
lines.append("\n## Interruptions and Failures\n")
for f in failures:
lines.append(f"- **{f['stage']}**: {f['status']}"
+ (f" -- {f['error']}" if "error" in f else ""))
else:
lines.append("\n## Result\n")
lines.append("All stages completed successfully.")
return "\n".join(lines) + "\n"
3. Activity-based depth adjustment
Scale analysis intensity to data volume. Avoid spending deep-analysis resources on tiny datasets, or shallow-scanning massive ones without prioritization.
def choose_depth(item_count, thresholds=None):
"""Select analysis depth tier based on item count.
Returns a dict of parameters that downstream stages consume.
Callers should NOT hardcode depth -- always derive from data.
"""
thresholds = thresholds or {
"shallow": (0, 100),
"standard": (101, 5000),
"deep": (5001, 50000),
"archival": (50001, None),
}
for tier, (lo, hi) in thresholds.items():
if hi is None and item_count >= lo:
return {"tier": tier, "sample_rate": 0.05, "full_analysis": False}
if lo <= item_count <= hi:
return {"tier": tier,
"sample_rate": 1.0 if tier in ("shallow","standard") else 0.2,
"full_analysis": tier in ("shallow", "standard")}
return {"tier": "standard", "sample_rate": 1.0, "full_analysis": True}
4. Rate-limit-aware fetching
import random
def fetch_with_backoff(fetch_fn, max_retries=5, base_delay=1.0):
"""Retry with exponential backoff + jitter. Respects rate limits."""
for attempt in range(max_retries):
try:
return fetch_fn()
except RateLimitError:
delay = base_delay * (2 ** attempt) + random.uniform(0, 1)
print(f" Rate limited. Waiting {delay:.1f}s (attempt {attempt+1})")
time.sleep(delay)
except Exception as e:
if attempt == max_retries - 1:
raise
time.sleep(base_delay)
raise RuntimeError(f"Failed after {max_retries} retries")
Handling Insufficient Data
| Situation | Action |
|---|
| Pipeline has nothing to process | Log "no input data", write report noting empty input, exit cleanly with status "no_data" |
| A stage produces empty output | Log warning, pass empty collection to next stage, do NOT skip or error. Let downstream stages handle empty input gracefully |
| External API unavailable | Retry with backoff. After max retries, mark stage as "degraded", continue with cached/partial data if available. Log what was skipped |
| Checkpoint file corrupted | Delete checkpoint, re-run stage from scratch. Log the corruption event |
Common Mistakes
| Mistake | Fix |
|---|
| No checkpointing on jobs > 5 min | Add checkpoint writes every N items and on stage completion |
| Monolithic single-function pipeline | Split into discrete stages with clear input/output contracts |
| Silently skipping failed items | Log every failure with item ID, error, timestamp |
| Hardcoded analysis depth | Use choose_depth() or equivalent based on actual item count |
| Retry without backoff | Always exponential backoff with jitter for external calls |
| Non-atomic checkpoint writes | Write to .tmp then rename (atomic on POSIX) |
| Ignoring partial failures | Collect errors, continue processing, summarize in report |
| No progress visibility | Log stage entry/exit, item counts, elapsed time |
Boundaries
SHOULD do:
- Manage pipeline stage sequencing and dependencies
- Handle interruptions gracefully (Ctrl+C saves checkpoint)
- Scale depth to data size automatically
- Log progress with structured, parseable output
- Produce a status report at
docs/analysis/03-automated-orchestration.md
Should NOT do:
- Make analytical conclusions (that is for domain-specific downstream skills)
- Silently skip failed stages without logging
- Retry indefinitely without backoff
- Decide what data means -- only move it through stages reliably
Report Output
The pipeline MUST end by writing its status to docs/analysis/03-automated-orchestration.md containing:
- Pipeline run timestamp
- Table of stages: name, status (complete/failed/interrupted/skipped), item count, error count, elapsed time
- Section listing any interruptions or skipped steps with reasons
- Summary of depth tier selected and why