Your pipeline is deployed and can materialize assets on demand. But for production use, you need automation—daily schedules that run without intervention and backfills to process historical data. This chapter covers making your pipeline fully autonomous.
Schedule Strategy
AIS data follows a predictable pattern:
- DMA: Publishes daily files around 03:00 UTC
- Data availability: Yesterday’s data available today
Our daily schedule will:
- Run at 06:00 UTC (safe margin after data publishes)
- Process yesterday’s partition (D-1 pattern)
- Materialize all assets (bronze → silver → validation → gold)
Creating Schedules
Define schedules in your pipeline code:
# src/pipeline/schedules.py
from dagster import (
schedule,
RunRequest,
SkipReason,
DefaultScheduleStatus,
)
from datetime import datetime, timedelta
@schedule(
cron_schedule="0 6 * * *", # 06:00 UTC every day
execution_timezone="UTC",
default_status=DefaultScheduleStatus.RUNNING,
)
def daily_dma_ingestion(context):
"""Daily DMA AIS data ingestion schedule.
Runs at 06:00 UTC to process yesterday's data (D-1 pattern).
DMA publishes daily files around 03:00 UTC, so 06:00 gives safe margin.
"""
scheduled_date = context.scheduled_execution_time
yesterday = scheduled_date - timedelta(days=1)
partition_key = yesterday.strftime("%Y-%m-%d")
return RunRequest(
run_key=f"dma_{partition_key}", # Unique, idempotent
partition_key=partition_key,
tags={
"schedule": "daily_dma_ingestion",
"source": "dma",
"partition_date": partition_key,
},
asset_selection=[
"dma_ais_bronze",
"dma_ais_silver",
"dma_silver_validation",
"vessel_tracks",
"port_density",
"daily_stats",
],
)
Conditional Schedules
Skip execution based on conditions:
@schedule(
cron_schedule="0 6 * * 1-5", # Monday-Friday only
execution_timezone="UTC",
default_status=DefaultScheduleStatus.STOPPED,
)
def weekday_only_ingestion(context):
"""DMA ingestion on weekdays only."""
scheduled_date = context.scheduled_execution_time
if scheduled_date.weekday() >= 5:
return SkipReason("Skipping weekend processing")
yesterday = scheduled_date - timedelta(days=1)
partition_key = yesterday.strftime("%Y-%m-%d")
return RunRequest(
run_key=f"dma_weekday_{partition_key}",
partition_key=partition_key,
asset_selection=["dma_ais_bronze", "dma_ais_silver"],
)
Smart Schedules with Data Availability Checks
def check_dma_data_available(partition_date: str) -> bool:
"""Check if DMA data is available for partition."""
import requests
url = f"https://web.ais.dk/aisdata/aisdk-{partition_date}.zip"
try:
response = requests.head(url, timeout=10)
return response.status_code == 200
except:
return False
@schedule(
cron_schedule="0 6 * * *",
execution_timezone="UTC",
default_status=DefaultScheduleStatus.STOPPED,
)
def smart_dma_ingestion(context):
"""Smart schedule that checks data availability first."""
yesterday = context.scheduled_execution_time - timedelta(days=1)
partition_key = yesterday.strftime("%Y-%m-%d")
if not check_dma_data_available(partition_key):
return SkipReason(f"DMA data not yet available for {partition_key}")
return RunRequest(
run_key=f"dma_smart_{partition_key}",
partition_key=partition_key,
asset_selection=["dma_ais_bronze", "dma_ais_silver"],
)
Registering Schedules
Add schedules to your Dagster definitions:
# src/pipeline/repository.py
from dagster import Definitions
from pipeline.assets import all_assets
from pipeline.schedules import daily_dma_ingestion
defs = Definitions(
assets=all_assets,
resources={
"minio_client": minio_resource,
"duckdb_conn": duckdb_resource,
},
schedules=[
daily_dma_ingestion,
],
)
Activating Schedules
Via Dagit UI
- Open Dagit → Automation tab
- Find
daily_dma_ingestion - Toggle switch to ON
- Check Next tick shows the correct time
Via CLI
# Start a schedule
dagster schedule start daily_dma_ingestion -m pipeline.repository
# Stop a schedule
dagster schedule stop daily_dma_ingestion -m pipeline.repository
# List all schedules
dagster schedule list -m pipeline.repository
# Preview next ticks
dagster schedule preview daily_dma_ingestion -m pipeline.repository
Verify in Kubernetes
kubectl logs -n ais-pipeline deployment/dagster-daemon --tail=100 | grep schedule
# Expected output:
# "Checking for new runs for schedule daily_dma_ingestion"
# "Launching run for schedule daily_dma_ingestion with partition 2024-01-15"
Backfills: Processing Historical Data
Want to process January 2024? Instead of triggering 31 manual runs, use backfills.
Via Dagit UI
- Open Assets tab
- Select assets to backfill
- Click Materialize → Launch a backfill
- Select partition range (Start:
2024-01-01, End:2024-01-31) - Click Launch backfill
Dagit shows:
- Progress: 0/31 completed
- Visual progress bar
- Duration elapsed
Via CLI
# Backfill a date range
dagster asset materialize \
--select dma_ais_bronze dma_ais_silver dma_silver_validation \
--partition-range 2024-01-01 2024-01-31 \
-m pipeline.repository
# Backfill single partition
dagster asset materialize \
--select dma_ais_bronze+ \
--partition 2024-01-15 \
-m pipeline.repository
The + suffix means “this asset and all downstream dependencies.”
Backfill Strategies
Sequential (default):
- One partition at a time
- Safer for resource-constrained systems
- Predictable memory usage
Parallel:
- Multiple partitions simultaneously
- Faster but uses more resources
Configure in Dagster daemon:
# k8s/dagster/values.yaml
dagsterDaemon:
env:
DAGSTER_BACKFILL_PARALLELISM: "4" # Process 4 partitions at once
Chunked:
- Process in batches (e.g., 7 days at a time)
- Balances speed and resource usage
def chunked_backfill(start_date, end_date, chunk_days=7):
"""Backfill in weekly chunks."""
current = datetime.strptime(start_date, "%Y-%m-%d")
end = datetime.strptime(end_date, "%Y-%m-%d")
while current <= end:
chunk_end = min(current + timedelta(days=chunk_days - 1), end)
print(f"Backfilling {current.date()} to {chunk_end.date()}")
# Run backfill for this chunk
# dagster asset materialize --partition-range ...
current = chunk_end + timedelta(days=1)
Idempotent Processing
Your pipeline should be idempotent—running the same partition twice produces the same result.
Why Idempotency Matters
- Retry failures: Re-run failed partitions without duplicates
- Late data: Reprocess when updated data arrives
- Schema evolution: Reprocess old data with new logic
Built-in Idempotency
Bronze Layer:
def download_day(self, target_date, skip_if_exists=True):
object_path = f"bronze/dma/aisdk-{target_date}.csv.gz"
if skip_if_exists:
try:
self.minio_client.stat_object("ais-lake", object_path)
return object_path # Already exists, skip
except:
pass
# Download...
Silver Layer:
- DuckDB
COPY ... TOoverwrites existing Parquet files - No duplicate data even if run twice
Gold Layer:
- Partitioned output overwrites same partition
- Aggregations are deterministic
Testing Idempotency
# Run partition twice
dagster asset materialize --select dma_ais_silver --partition 2024-01-15
dagster asset materialize --select dma_ais_silver --partition 2024-01-15
# Check MinIO: Same file, not duplicate
mc ls ais-minio/ais-lake/silver/ais/source=dma/dt=2024-01-15/
Handling Late-Arriving Data
When data is republished with corrections:
Manual Reprocessing
dagster asset materialize --select dma_ais_bronze+ --partition 2024-01-15
Sensor for Updated Data
from dagster import sensor, RunRequest
@sensor(minimum_interval_seconds=3600)
def dma_update_sensor(context):
"""Detect when DMA republishes files."""
updated_partitions = []
# Check last 30 days for file updates
for days_ago in range(30):
date = datetime.now() - timedelta(days=days_ago)
partition_key = date.strftime("%Y-%m-%d")
current_etag = get_file_etag(f"bronze/dma/aisdk-{partition_key}.csv.gz")
last_etag = context.cursor
if current_etag != last_etag:
updated_partitions.append(partition_key)
if updated_partitions:
return [
RunRequest(
run_key=f"dma_update_{p}",
partition_key=p,
asset_selection=["dma_ais_bronze", "dma_ais_silver"],
)
for p in updated_partitions
]
Monitoring Scheduled Runs
Dagit UI
Runs Tab:
- Filter:
tags:schedule=daily_dma_ingestion - Check failure rate and duration trends
Automation Tab:
- Schedule tick history
- Next scheduled execution
- Skipped ticks (indicates daemon issues)
Prometheus Alerts
- alert: AISScheduledRunFailed
expr: increase(ais_asset_failures_total{asset_name="dma_ais_silver"}[1h]) > 0
for: 5m
labels:
severity: warning
annotations:
summary: "Scheduled AIS run failed"
- alert: AISScheduleMissed
expr: (time() - dagster_schedule_last_tick_timestamp{schedule_name="daily_dma_ingestion"}) > 86400
for: 1h
labels:
severity: critical
annotations:
summary: "AIS schedule not running"
description: "Schedule hasn't run in 24 hours"
Best Practices
Partition Naming
Use ISO 8601 format: YYYY-MM-DD
- Sorts correctly
- Unambiguous
- SQL compatible
Schedule Timing
- After data availability: Schedule 3+ hours after source publishes
- Before business hours: Complete before users need data
- Stagger schedules: Don’t run everything at once
Error Handling
- Transient failures: Retry automatically (schedule runs tomorrow)
- Persistent failures: Alert, investigate, fix, re-run
- Partial failures: Asset-level isolation
Data Retention
- Bronze: Delete after 7 days (temporary staging)
- Silver: Keep indefinitely (source of truth)
- Gold: Keep indefinitely (or archive yearly)
What You’ve Built
Your pipeline now runs autonomously:
- Daily schedules: Automatic D-1 ingestion at 06:00 UTC
- Backfills: Process historical data in bulk
- Idempotency: Safe reprocessing of partitions
- Late data handling: Reprocess updated partitions
- Monitoring: Track scheduled runs and failures
What’s Next
With automation in place, Chapter 12 covers day-to-day operations—health checks, troubleshooting, and maintenance tasks.
Building automated data pipelines? We help organizations design and implement hands-free data infrastructure. Get in touch to discuss your project.
Comments