🆚 Runnable vs Metaflow: Capability Comparison¶
Both Runnable and Metaflow solve ML pipeline orchestration with different approaches. Here's a side-by-side comparison using a real ML workflow.
The Example: Existing ML Functions¶
Let's start with typical Python functions you might already have:
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
import xgboost as xgb
import joblib
def load_and_clean_data():
"""Your existing data loading function."""
customers = pd.read_csv("s3://bucket/raw-data/customers.csv")
transactions = pd.read_csv("s3://bucket/raw-data/transactions.csv")
data = customers.merge(transactions, on="customer_id").dropna()
X = data.drop(['target'], axis=1)
y = data['target']
X.to_csv("features.csv", index=False)
y.to_csv("target.csv", index=False)
return {"n_samples": len(X), "n_features": X.shape[1]}
def train_random_forest(n_samples, n_features, max_depth=10):
"""Your existing RF training function."""
X = pd.read_csv("features.csv")
y = pd.read_csv("target.csv").values.ravel()
model = RandomForestClassifier(max_depth=max_depth, random_state=42)
model.fit(X, y)
joblib.dump(model, "rf_model.pkl")
return {"model_type": "RandomForest", "accuracy": model.score(X, y)}
def train_xgboost(n_samples, n_features, max_depth=10):
"""Your existing XGBoost training function."""
X = pd.read_csv("features.csv")
y = pd.read_csv("target.csv").values.ravel()
model = xgb.XGBClassifier(max_depth=max_depth, random_state=42)
model.fit(X, y)
joblib.dump(model, "xgb_model.pkl")
return {"model_type": "XGBoost", "accuracy": model.score(X, y)}
def select_best_model(model_results):
"""Your existing model selection function."""
best_model = max(model_results, key=lambda x: x['accuracy'])
# Copy best model logic...
return best_model
Goal: Create a pipeline that runs these functions with parallel model training.
Making It Work with Runnable¶
Work required: Add pipeline wrapper (functions stay unchanged)
from runnable import Pipeline, PythonTask, Parallel
# Import your existing functions (no changes needed)
from your_ml_code import load_and_clean_data, train_random_forest, train_xgboost, select_best_model
def main():
pipeline = Pipeline(steps=[
PythonTask(function=load_and_clean_data, returns=["n_samples", "n_features"]),
Parallel(branches={
"rf": PythonTask(function=train_random_forest, returns=["rf_results"]).as_pipeline(),
"xgb": PythonTask(function=train_xgboost, returns=["xgb_results"]).as_pipeline()
}),
PythonTask(function=select_best_model, returns=["best_model"])
])
pipeline.execute()
return pipeline # Required for Runnable
if __name__ == "__main__":
main()
That's it. Functions unchanged, single wrapper file.
Making It Work with Metaflow¶
Work required: Convert functions to FlowSpec class structure
Functions Can Stay External:
# your_ml_code.py (functions unchanged)
def load_and_clean_data():
# Your existing logic stays the same
return {"n_samples": 1000, "n_features": 20}
def train_random_forest(n_samples, n_features, max_depth=10):
# Your existing logic stays the same
return {"model_type": "RandomForest", "accuracy": 0.95}
Metaflow requires FlowSpec wrapper:
from metaflow import FlowSpec, step, Parameter
# Import your existing functions (no changes needed)
from your_ml_code import load_and_clean_data, train_random_forest, train_xgboost, select_best_model
class MLTrainingFlow(FlowSpec):
max_depth = Parameter('max_depth', default=15)
@step
def start(self):
# Call your existing function directly
data_stats = load_and_clean_data()
self.n_samples = data_stats['n_samples']
self.n_features = data_stats['n_features']
self.next(self.train_models, foreach=['RandomForest', 'XGBoost'])
@step
def train_models(self):
# Call your existing functions directly
if self.input == 'RandomForest':
results = train_random_forest(self.n_samples, self.n_features, self.max_depth)
else:
results = train_xgboost(self.n_samples, self.n_features, self.max_depth)
self.model_results = results
self.next(self.select_best)
@step
def select_best(self, inputs):
model_results = [input.model_results for input in inputs]
self.best = select_best_model(model_results) # Call your existing function
self.next(self.end)
@step
def end(self):
pass
Running the Pipeline:
Core Capabilities Comparison¶
Workflow Features¶
| Feature | Runnable Approach | Metaflow Approach |
|---|---|---|
| Pipeline Definition | Single Python file with minimal setup | FlowSpec class with decorators |
| Task Types | Python, Notebooks, Shell, Stubs | Python steps with flow state |
| Parameter Configuration | YAML/JSON config files via parameters_file |
Config files and command-line parameters |
| Parallel Execution | Parallel() with explicit branching |
foreach parameter for fan-out execution |
| Conditional Logic | Native Conditional() support |
Manual implementation in step logic |
| Map/Reduce | Native Map() with custom reducers |
foreach with join steps for result aggregation |
Data Handling¶
| Feature | Runnable Approach | Metaflow Approach |
|---|---|---|
| File Management | Automatic file sync via Catalog(put/get) |
Manual file I/O - no catalog system |
| Data Versioning | Content-based hashing for change detection | Automatic versioning via Metaflow datastore (Python objects only) |
| Storage Backends | File, S3, Minio via plugins | Local, S3, Azure, GCP datastores |
| Data Lineage | Automatic via run logs | Rich lineage through Metaflow UI |
Production Deployment¶
| Feature | Runnable Approach | Metaflow Approach |
|---|---|---|
| Environment Portability | Same code runs local/container/K8s/Argo | Same FlowSpec runs local/AWS/K8s with --with flags |
| AWS Integration | Manual configuration required | Native AWS Batch, Step Functions integration |
| Monitoring | Basic run logs and timeline visualization | Rich Metaflow UI with execution graphs |
| Extensibility | Entry points auto-discovery for custom task types, executors, catalogs | Limited plugin system - primarily configuration-based extensions |
When to Choose Each Tool¶
Choose Runnable When:¶
- Working with existing Python functions without refactoring
- Need multi-environment portability (local → container → K8s → Argo)
- Require advanced workflow patterns (parallel, conditional, map-reduce)
- Want immediate productivity with minimal setup
- Working with mixed task types (Python + notebooks + shell)
Choose Metaflow When:¶
- Need rich execution visualization and monitoring
- Heavy investment in AWS services and infrastructure
- Managing hundreds/thousands of concurrent workflows
- Want automatic Python object serialization between steps
- Already familiar with decorator-based patterns
- Need built-in experiment tracking and comparison
Implementation Structure Comparison¶
Runnable Approach:
- Minimal disruption: Wrap existing functions directly without changes
- Single file: Complete pipeline in one Python file
- No restructuring: Keep your current code organization and patterns
- Optional infrastructure: Add AWS/K8s configs only when needed for specific environments
Metaflow Approach:
- Function restructuring: Convert existing functions to fit FlowSpec class patterns
- Decorator-based: Use
@stepand@paralleldecorators for flow control - Flow state management: Store data in
selfattributes between steps - Infrastructure integration: Built-in AWS Batch, Step Functions, S3 datastore
Next: See how Runnable compares to Kedro and other orchestration tools.