Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Data pipeline orchestration expertise covering Airflow DAG design, dbt models, pipeline patterns (batch, micro-batch, streaming), dependency management, idempotency, backfill strategies, data lineage tracking, and monitoring for building reliable, observable data workflows. Use when the user asks about data pipeline, data pipeline best practices, or needs guidance on data pipeline implementation. Do NOT use when the user needs a different specialized skill or is asking about an unrelated technol
.claude/skills/ferroxlabs-data-pipeline/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-11 | ✗→✓ | ▲ Improved | 152% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 134% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 51% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 92% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 125% | 0% |
Data pipeline orchestration is the discipline of scheduling, coordinating, and monitoring data workflows that move and transform data across systems. This skill covers the design principles, tools, and patterns needed to build pipelines that are reliable, observable, and maintainable.
pythonfrom datetime import datetime, timedelta from airflow import DAG from airflow.operators.python import PythonOperator from airflow.operators.empty import EmptyOperator from airflow.providers.common.sql.operators.sql import SQLExecuteQueryOperator from airflow.utils.task_group import TaskGroup from airflow.models import Variable # DAG-level defaults default_args = { 'owner': 'data-engineering', 'depends_on_past': False, 'email_on_failure': True, 'email_on_retry': False, # ... (condensed) ... task_id='check_freshness', python_callable=validate_freshness, ) start >> extract_group >> transform_group >> load_group >> validate_group >> end
python# Push data (small values only - max ~48KB in metadata DB) def extract_task(**context): record_count = run_extraction() context['ti'].xcom_push(key='record_count', value=record_count) return {'status': 'success', 'file_path': 's3://bucket/staging/extract.parquet'} # Pull data def transform_task(**context): extract_result = context['ti'].xcom_pull(task_ids='extract_crm') file_path = extract_result['file_path'] record_count = context['ti'].xcom_pull(task_ids='extract_crm', key='record_count')
python@dag(schedule='@daily', start_date=datetime(2024, 1, 1)) def process_regions(): @task def get_active_regions(): return ['us-east', 'us-west', 'eu-west', 'ap-southeast'] @task def process_region(region: str): # This runs once per region, in parallel return extract_and_transform(region) @task def consolidate(results): # Receives list of all results return merge_results(results) regions = get_active_regions() results = process_region.expand(region=regions) consolidate(results)
pythonfrom airflow.operators.python import BranchPythonOperator def choose_branch(**context): execution_date = context['ds'] day_of_week = datetime.strptime(execution_date, '%Y-%m-%d').weekday() if day_of_week == 0: # Monday return 'full_refresh' return 'incremental_load' branch = BranchPythonOperator( task_id='choose_load_strategy', python_callable=choose_branch, ) full_refresh = PythonOperator(task_id='full_refresh', ...) incremental_load = PythonOperator(task_id='incremental_load', ...) merge_results = EmptyOperator(task_id='merge', trigger_rule='none_failed_min_one_success') branch >> [full_refresh, incremental_load] >> merge_results
dbt_project/
dbt_project.yml
packages.yml
models/
staging/ # 1:1 with source tables, light transformations
stg_stripe/
_stg_stripe__models.yml
stg_stripe__customers.sql
stg_stripe__payments.sql
stg_salesforce/
_stg_salesforce__models.yml
stg_salesforce__contacts.sql
intermediate/ # Business logic, joins, complex transforms
int_customer_payments_pivoted.sql
# ... (condensed) ...
assert_total_revenue_matches_stripe.sql
seeds/
country_codes.csv
snapshots/
snap_customer.sqlsql-- models/staging/stg_stripe/stg_stripe__customers.sql {{ config( materialized='view', tags=['stripe', 'daily'] ) }} WITH source AS ( SELECT * FROM {{ source('stripe', 'customers') }} ), renamed AS ( SELECT # ... (condensed) ... OR o._loaded_at > (SELECT MAX(updated_at) FROM {{ this }}) {% endif %} ) SELECT * FROM final
yaml# models/marts/core/_core__models.yml version: 2 models: - name: dim_customer description: > One row per customer. Combines Stripe customer data with order history and support ticket metrics. columns: - name: customer_id description: Unique customer identifier from Stripe data_tests: - unique - not_null # ... (condensed) ... - name: total_orders data_tests: - dbt_expectations.expect_column_values_to_be_between: min_value: 0 max_value: 100000
Schedule -> Extract -> Stage -> Transform -> Load -> Validate -> Notify
|
(on failure)
|
Dead Letter QueueBest for: Daily/hourly reporting, warehouse loads, ML feature computation
Continuous Loop:
1. Poll for new data (every 1-15 minutes)
2. If data exists: process batch
3. Checkpoint progress
4. Sleep until next pollpython# Micro-batch with Airflow sensor from airflow.sensors.sql import SqlSensor wait_for_data = SqlSensor( task_id='wait_for_new_records', conn_id='source_db', sql=""" SELECT COUNT(*) FROM events WHERE created_at > '{{ prev_data_interval_end_success }}' """, mode='reschedule', # Free up worker slot while waiting poke_interval=300, # Check every 5 minutes timeout=3600, # Give up after 1 hour )
Real-time path (speed layer):
Source -> Kafka -> Stream Processor -> Serving Layer (hot data)
Batch path (batch layer):
Source -> Data Lake -> Batch Processor -> Serving Layer (complete data)
Serving layer merges both views for queriesSource -> Kafka (immutable log) -> Stream Processor -> Serving Layer
|
Reprocessing: replay from Kafka offset 0pythonfrom airflow.sensors.external_task import ExternalTaskSensor # Wait for upstream DAG to complete wait_for_upstream = ExternalTaskSensor( task_id='wait_for_raw_data_load', external_dag_id='raw_data_ingestion', external_task_id='load_complete', execution_date_fn=lambda dt: dt, # Same logical date mode='reschedule', timeout=7200, )
pythonfrom airflow import Dataset # Producer DAG: declares output dataset raw_customers = Dataset("s3://bucket/raw/customers/") with DAG('ingest_customers', schedule='@hourly') as producer_dag: ingest = PythonOperator( task_id='ingest', python_callable=ingest_fn, outlets=[raw_customers], # Declares this task produces this dataset ) # Consumer DAG: triggered when dataset is updated with DAG('transform_customers', schedule=[raw_customers]) as consumer_dag: transform = PythonOperator( task_id='transform', python_callable=transform_fn, )
Every pipeline run must produce the same result regardless of how many times it executes.
python# Pattern 1: Partition overwrite def idempotent_load(spark, source_path, target_path, partition_date): df = spark.read.parquet(source_path) df.write \ .mode("overwrite") \ .partitionBy("date") \ .option("partitionOverwriteMode", "dynamic") \ .parquet(target_path) # Pattern 2: DELETE + INSERT in transaction def idempotent_sql_load(engine, staging_table, target_table, partition_col, partition_val): with engine.begin() as conn: conn.execute(f""" DELETE FROM {target_table} # ... (condensed) ... WHERE {partition_col} = '{partition_val}' """) # Pattern 3: MERGE/UPSERT with deterministic output # The result after N runs is identical to the result after 1 run
python# ALWAYS use logical execution date, NEVER wall-clock time def extract_fn(**context): # Correct: process data for the logical execution window start = context['data_interval_start'] end = context['data_interval_end'] query = f""" SELECT * FROM events WHERE event_time >= '{start}' AND event_time < '{end}' """ # This is idempotent: same execution date always queries same window
shell# CLI backfill for a date range airflow dags backfill \ --start-date 2024-01-01 \ --end-date 2024-06-30 \ --reset-dagruns \ customer_360_daily # Limit parallelism during backfill airflow dags backfill \ --start-date 2024-01-01 \ --end-date 2024-06-30 \ --max-active-runs 3 \ customer_360_daily
yaml# dbt exposure for lineage documentation version: 2 exposures: - name: weekly_revenue_dashboard type: dashboard maturity: high url: [reference URL] description: Executive weekly revenue dashboard depends_on: - ref('fct_revenue') - ref('dim_customer') - ref('dim_product') owner: name: Analytics Team email: analytics@company.com
python# Simple lineage metadata capture class LineageTracker: def __init__(self, catalog_api): self.catalog = catalog_api def record_transformation(self, job_id, inputs, outputs, transformation_type): lineage_event = { 'job_id': job_id, 'timestamp': datetime.utcnow().isoformat(), 'inputs': [{'dataset': i, 'type': 'read'} for i in inputs], 'outputs': [{'dataset': o, 'type': 'write'} for o in outputs], 'transformation_type': transformation_type, 'run_id': str(uuid.uuid4()), } self.catalog.emit_lineage(lineage_event)
| Metric | Alert Threshold | Action | |--------|----------------|--------| | Pipeline duration | >2x historical p95 | Investigate resource contention or data growth | | Task failure rate | >0% for critical paths | Page on-call engineer | | Data freshness | >SLA target | Escalate to pipeline owner | | Row count deviation | >20% from historical | Investigate source system changes | | Schema drift | Any unexpected column | Block pipeline, notify data owner |
pythonfrom airflow.providers.slack.operators.slack_webhook import SlackWebhookOperator def on_failure_callback(context): task_instance = context['task_instance'] dag_id = context['dag'].dag_id task_id = task_instance.task_id execution_date = context['execution_date'] log_url = task_instance.log_url SlackWebhookOperator( task_id='slack_alert', slack_webhook_conn_id='slack_data_eng', message=f""" :red_circle: Pipeline Failure *DAG*: {dag_id} *Task*: {task_id} *Execution Date*: {execution_date} *Log*: {log_url} """, ).execute(context)
Use this skill when:
Do NOT use this skill when:
markdown# Data Pipeline Analysis ## Context Assessment [Situation summary and constraints] ## Recommended Approach [Primary recommendation with rationale] ## Implementation Steps 1. [Step with specific details] 2. [Step with specific details] 3. [Step with specific details] ## Trade-offs and Considerations - [Key trade-off 1] - [Key trade-off 2] ## Next Steps - [Immediate action item] - [Follow-up action item]
Input: "Help me implement data pipeline for a medium-scale production application"
Output: A structured analysis covering current state assessment, recommended data pipeline approach with specific patterns, implementation roadmap with milestones, and risk mitigation strategies tailored to the application scale and constraints.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 38,610 | 27,347 | -29% | 1 | 1 | 0% | 4,638 | 6,993 | +51% | 0 | 0 | — |
case-02 | fail→fail | 32,754 | 32,330 | -1% | 1 | 1 | 0% | 4,023 | 8,387 | +108% | 0 | 0 | — |
case-03 | pass→pass | 31,014 | 29,853 | -4% | 1 | 1 | 0% | 3,828 | 7,344 | +92% | 0 | 0 | — |
case-04 | pass→pass | 24,421 | 22,191 | -9% | 1 | 1 | 0% | 2,981 | 6,710 | +125% | 0 | 0 | — |
case-05 | pass→pass | 20,135 | 23,220 | +15% | 1 | 1 | 0% | 3,100 | 6,855 | +121% | 0 | 0 | — |
case-06 | pass→pass | 24,273 | 17,425 | -28% | 1 | 1 | 0% | 2,800 | 5,521 | +97% | 0 | 0 | — |
case-07 | pass→pass | 27,002 | 23,610 | -13% | 1 | 1 | 0% | 3,777 | 6,704 | +77% | 0 | 0 | — |
case-08 | pass→pass | 23,716 | 24,629 | +4% | 1 | 1 | 0% | 2,886 | 6,828 | +137% | 0 | 0 | — |
case-24 | pass→pass | 24,783 | 22,950 | -7% | 1 | 1 | 0% | 3,991 | 7,682 | +92% | 0 | 0 | — |
case-09 | pass→pass | 34,679 | 27,216 | -22% | 1 | 1 | 0% | 4,492 | 6,822 | +52% | 0 | 0 | — |
case-10 | pass→pass | 24,378 | 26,084 | +7% | 1 | 1 | 0% | 3,742 | 7,094 | +90% | 0 | 0 | — |
case-11 | fail→pass | 22,512 | 24,701 | +10% | 1 | 1 | 0% | 2,643 | 6,661 | +152% | 0 | 0 | — |
case-12 | pass→pass | 19,083 | 24,061 | +26% | 1 | 1 | 0% | 2,483 | 6,276 | +153% | 0 | 0 | — |
case-13 | fail→pass | 22,497 | 22,685 | +1% | 1 | 1 | 0% | 2,695 | 6,301 | +134% | 0 | 0 | — |
case-14 | pass→pass | 24,492 | 21,061 | -14% | 1 | 1 | 0% | 3,003 | 6,531 | +117% | 0 | 0 | — |
case-15 | pass→pass | 29,923 | 22,443 | -25% | 1 | 1 | 0% | 3,432 | 6,575 | +92% | 0 | 0 | — |
case-16 | pass→pass | 20,079 | 19,033 | -5% | 1 | 1 | 0% | 2,632 | 6,032 | +129% | 0 | 0 | — |
case-17 | pass→pass | 28,491 | 22,782 | -20% | 1 | 1 | 0% | 3,549 | 6,200 | +75% | 0 | 0 | — |
case-18 | pass→pass | 40,684 | 36,907 | -9% | 1 | 1 | 0% | 3,519 | 7,229 | +105% | 0 | 0 | — |
case-19 | pass→pass | 20,294 | 19,747 | -3% | 1 | 1 | 0% | 2,471 | 6,301 | +155% | 0 | 0 | — |
case-20 | pass→pass | 15,927 | 18,610 | +17% | 1 | 1 | 0% | 2,597 | 5,957 | +129% | 0 | 0 | — |
case-21 | pass→pass | 38,120 | 23,202 | -39% | 1 | 1 | 0% | 5,558 | 7,433 | +34% | 0 | 0 | — |
case-22 | pass→pass | 18,400 | 15,818 | -14% | 1 | 1 | 0% | 2,947 | 6,260 | +112% | 0 | 0 | — |
case-23 | pass→pass | 12,332 | 14,689 | +19% | 1 | 1 | 0% | 1,884 | 6,341 | +237% | 0 | 0 | — |
case-25 | pass→pass | 36,496 | 31,891 | -13% | 1 | 1 | 0% | 5,951 | 9,158 | +54% | 0 | 0 | — |
case-26 | pass→pass | 19,327 | 19,371 | +0% | 1 | 1 | 0% | 2,937 | 6,763 | +130% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 26 cases were attempted. The headline lift of +8 percentage points is the difference between those two pass rates over the 26 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.