Guide
This guide walks you through building a real pipeline with barca, from a single function to a multi-stage DAG with sensors, tasks, and partitions.
Prerequisites
Section titled “Prerequisites”- Python >= 3.12
- barca installed (
pip install barca)
Verify it works:
barca --help1. Your first asset
Section titled “1. Your first asset”An asset is a Python function that returns data. That’s it.
Create a file called pipeline.py:
from barca import asset
@asset()def greeting() -> dict: return {"message": "Hello from barca!"}Run it:
barca get pipeline.pyYou’ll see output like:
{"elapsed_seconds":0.039,"final_output":{"message":"Hello from barca!"},"phases":1,"run_id":"b1b1ff29d6cc","steps_executed":1}And on stderr, a one-line progress summary:
[barca] 1/1 steps done in 0.0sWhat just happened?
- The Rust binary parsed
pipeline.pyusing ruff’s AST parser (no import, pure text analysis) - Found one
@asset()decorator, extracted the function name and metadata - Built a trivial DAG (one node, no edges)
- Generated an execution plan with one phase
- Spawned a Python worker, which imported your module and called
greeting() - Collected the return value and persisted it to
.barca/metadata.db
The @asset() decorator itself does nothing at runtime – it’s an identity function. Your code runs exactly the same with or without barca installed.
2. Dependencies between assets
Section titled “2. Dependencies between assets”Assets can depend on other assets via inputs=. Barca resolves the DAG and executes them in the right order.
from barca import asset
@asset()def raw_data() -> list[dict]: return [ {"name": "Alice", "score": 92}, {"name": "Bob", "score": 85}, {"name": "Carol", "score": 97}, ]
@asset(inputs={"data": raw_data})def summary(data: list[dict]) -> dict: scores = [d["score"] for d in data] return { "count": len(scores), "mean": sum(scores) / len(scores), "top": max(data, key=lambda d: d["score"])["name"], }barca get pipeline.pyBarca sees that summary depends on raw_data, so it:
- Puts both in the same phase, same worker stream — a linear chain has no parallelism to gain from a phase split
- Runs
raw_datafirst in that stream’s worker - Hands
raw_data’s output straight tosummaryin the same process - Runs
summarywith the data injected as thedatakwarg
You can inspect the plan without running anything:
barca plan pipeline.py{ "total_steps": 2, "phases": [ { "reason": "Initial", "streams": [ {"stream_id": "p0-w0", "steps": ["pipeline.py:raw_data", "pipeline.py:summary"]} ] } ]}3. Parallel execution
Section titled “3. Parallel execution”When assets are independent (no edges between them), barca runs them in parallel as separate worker streams within the same phase.
from barca import asset
@asset()def users() -> list[dict]: return [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
@asset()def products() -> list[dict]: return [{"id": 1, "name": "Widget"}, {"id": 2, "name": "Gadget"}]
@asset()def orders() -> list[dict]: return [{"user_id": 1, "product_id": 2, "qty": 3}]
@asset(inputs={"users": users, "products": products, "orders": orders})def report(users: list[dict], products: list[dict], orders: list[dict]) -> dict: return { "total_users": len(users), "total_products": len(products), "total_orders": len(orders), }The plan will look like:
Phase 1 (Initial): users, products, orders ← 3 parallel streamsPhase 2 (FanIn): report ← waits for all 3Barca spawns up to available_parallelism() workers per phase. On an 8-core machine, all three source assets run concurrently.
4. Diamond DAGs
Section titled “4. Diamond DAGs”Real pipelines aren’t linear chains. They fork and join. Barca handles this naturally.
from barca import asset
@asset()def raw_sales() -> list[dict]: return [{"product": "A", "amount": 100}, {"product": "B", "amount": 200}]
@asset()def raw_inventory() -> list[dict]: return [{"product": "A", "stock": 50}, {"product": "B", "stock": 10}]
@asset(inputs={"sales": raw_sales})def clean_sales(sales: list[dict]) -> list[dict]: return [s for s in sales if s["amount"] > 0]
@asset(inputs={"inventory": raw_inventory})def clean_inventory(inventory: list[dict]) -> list[dict]: return [i for i in inventory if i["stock"] > 0]
@asset(inputs={"sales": clean_sales, "inventory": clean_inventory})def dashboard(sales: list[dict], inventory: list[dict]) -> dict: return { "total_revenue": sum(s["amount"] for s in sales), "low_stock": [i["product"] for i in inventory if i["stock"] < 20], }The execution plan:
Phase 1 (Initial): raw_sales → clean_sales ← one stream, one worker, chained raw_inventory → clean_inventory ← parallel stream, chainedPhase 2 (FanIn): dashboard ← waits for both chainsEach source-and-its-transform pair is a linear chain, so barca runs it as one
worker stream rather than splitting it across phases; the two chains still run
in parallel with each other. dashboard depends on both chains’ outputs, so it
gets its own fan-in phase.
5. Sensors
Section titled “5. Sensors”Sensors observe external state. They’re source nodes in the DAG that return (update_detected, output).
from barca import asset, sensor
@sensor()def check_inbox() -> tuple[bool, list[str]]: from pathlib import Path files = list(Path("inbox").glob("*.csv")) return bool(files), [str(f) for f in files]
@asset(inputs={"files": check_inbox})def process_inbox(files: list[str]) -> dict: return {"processed": len(files), "files": files}Sensors are never cached – they always re-run. The worker unpacks the (update_detected, output) tuple automatically, so a downstream asset’s kwarg receives just output (as in files: list[str] above), not the tuple.
6. Tasks
Section titled “6. Tasks”Tasks handle side effects – deploying, notifying, writing to external systems. They always re-run and are never cached.
from barca import asset, task
@asset()def daily_report() -> dict: return {"revenue": 42000, "orders": 150}
@task(inputs={"report": daily_report})def send_slack_notification(report: dict) -> None: # In production, this would call Slack's API print(f"Daily revenue: ${report['revenue']:,}")
@task(inputs={"report": daily_report})def write_to_s3(report: dict) -> None: # In production, this would upload to S3 print(f"Uploading report with {report['orders']} orders")Both tasks run in the same phase (they’re independent of each other) after daily_report completes. Use barca run to execute tasks.
barca run always force-reruns every upstream asset in the task’s cone (it “bursts” the cache) – so daily_report re-executes on every barca run send_slack_notification pipeline.py, even if it’s already cached. Pass --burst report_name_a,report_name_b to burst only specific upstream assets and leave the rest cache-aware.
7. Partitions
Section titled “7. Partitions”Partitions fan a single asset definition into N independent runs, one per partition key.
from barca import asset, partitions, collect
@asset(partitions={"region": partitions(["us-east", "us-west", "eu-west"])})def regional_sales(region: str) -> dict: # In production, this would query a database filtered by region return {"region": region, "total": hash(region) % 10000}
@asset(inputs={"sales": collect(regional_sales)})def global_summary(sales: dict) -> dict: total = sum(v["total"] for v in sales.values()) return {"regions": len(sales), "global_total": total}regional_salesruns 3 times, once per regioncollect(regional_sales)aggregates all partition outputs into a single dictglobal_summaryreceives all three results at once
8. Multi-file pipelines
Section titled “8. Multi-file pipelines”Barca can parse multiple Python files. Assets can reference functions across files as long as all files are passed to the CLI.
my_project/ sources.py # @asset defs for raw data transforms.py # @asset defs that depend on sources tasks.py # @task defsbarca get my_project/sources.py my_project/transforms.py my_project/tasks.pyBarca merges all discovered nodes into a single DAG and plans execution across the full graph.
9. Freshness markers
Section titled “9. Freshness markers”Control when assets should re-run:
import time
from barca import asset, Always, Manual, Schedule
@asset(freshness=Always())def always_fresh() -> dict: """Re-runs on every reconcile cycle.""" return {"ts": time.time()}
@asset(freshness=Manual())def on_demand() -> dict: """Only runs when explicitly triggered.""" return {"manual": True}
@asset(freshness=Schedule("0 5 * * *"))def daily_at_5am() -> dict: """Eligible for execution at 5 AM daily.""" return {"scheduled": True}10. Inspecting plans
Section titled “10. Inspecting plans”barca plan is your debugging tool. It shows you exactly what barca will do without executing anything.
# See the plan as formatted JSONbarca plan pipeline.py | python -m json.tool
# Count total stepsbarca plan pipeline.py | python -c "import json,sys; print(json.load(sys.stdin)['total_steps'])"The plan shows:
- Phases: groups of work that execute sequentially
- Streams: parallel workers within a phase
- Steps: individual asset functions within a stream
- Reason: why a phase boundary exists (
Initialfor the first phase, orFanInwhen a step needs outputs from multiple prior streams)
Putting it together
Section titled “Putting it together”Here’s a complete pipeline that uses everything:
from barca import asset, sensor, task, partitions, collect
# Sensor: poll for new data@sensor()def check_data_lake() -> tuple[bool, dict]: # Check if new parquet files landed return True, {"path": "s3://bucket/raw/", "files": 3}
# Source assets (parallel)@asset(partitions={"table": partitions(["users", "events", "purchases"])})def extract(table: str) -> dict: return {"table": table, "rows": 1000}
# Transform (runs per partition, inheriting from extract)@asset(inputs={"raw": extract})def transform(raw: dict) -> dict: return {"table": raw["table"], "clean_rows": raw["rows"] - 10}
# Aggregate all partitions@asset(inputs={"tables": collect(transform)})def merge(tables: dict) -> dict: total = sum(v["clean_rows"] for v in tables.values()) return {"total_rows": total, "tables": len(tables)}
# Sensor-driven asset@asset(inputs={"lake_status": check_data_lake, "data": merge})def report(lake_status: dict, data: dict) -> dict: # The worker already unpacked check_data_lake's (update_detected, output) # tuple, so lake_status here is just the sensor's output dict. return {"source": lake_status["path"], **data}
# Side-effect task@task(inputs={"report": report})def notify(report: dict) -> None: print(f"Pipeline complete: {report['total_rows']} rows from {report['tables']} tables")barca plan pipeline.py # inspect the execution planbarca get pipeline.py # run it-
Decorators are no-ops. Your code works without barca installed.
from barca import assetimports an identity function. This means you can unit test your functions normally. -
Outputs are serialized between phases. By default barca uses JSON, so dicts, lists, strings, numbers, and booleans work with no extra configuration. Two escape hatches beyond that:
@asset(serializer="pickle")for large or non-JSON-serializable plain-Python payloads (faster than JSON for large list-of-dict structures too — seebenchmarks/RESULTS.md’setl_duckdbnotes), or return a pandas/polars DataFrame and barca automatically serializes it as parquet — noserializer=needed, and it’s the fastest option for tabular data (vectorized columnar (de)serialization instead of row-by-row). -
Use
barca planliberally. It’s free (no execution) and shows you exactly how barca decomposes your DAG. -
Check
.barca/metadata.db. It’s a SQLite database. You can query it directly:Terminal window sqlite3 .barca/metadata.db "SELECT node_id, status, created_at FROM materializations ORDER BY created_at DESC LIMIT 10" -
Stderr is for diagnostics. Barca prints timing and topology info to stderr. Stdout is reserved for structured JSON output. Pipe stdout to
jqfor clean formatting:Terminal window barca get pipeline.py 2>/dev/null | jq .