Dataops workflow orchestration review
Профессиональные Data Engineering Agent Skills для разработки AI Agentic Data Platform
npx -y skills add ivanshamaev/de-agent-skills --skill dataops_workflow_orchestration_reviewAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 13 stars13 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Workflow orchestration comparison and review — Airflow vs Prefect vs Dagster vs Temporal (architecture, execution model, dependency management, observability), when to choose each, migration patterns from Airflow to Prefect/Dagster, orchestrator evaluation scorecard, event-driven vs schedule-based triggering, Software-Defined Assets (Dagster) vs task-based (Airflow), hybrid orchestration patterns
SKILL.md
10.1 KB, as published. Nobody here has run it
Workflow Orchestration Review
When to Use
- Selecting an orchestration platform for a new data platform
- Evaluating migration from Airflow to a modern alternative
- Comparing orchestration capabilities for a specific use case
- Reviewing architecture of existing orchestration setup
- Designing event-driven pipeline triggers
Orchestrator Comparison
| Feature | Airflow | Prefect | Dagster | Temporal |
|---|---|---|---|---|
| Model | DAG task graph | Flow/task functions | Asset-centric | Durable workflow functions |
| Scheduling | Cron + sensors | Deployments + schedules | Partitions + schedules | Timer signals |
| State store | PostgreSQL | PostgreSQL/SQLite | PostgreSQL/SQLite | Cassandra/PostgreSQL |
| Execution | Distributed (K8s/Celery) | Work pools (K8s/Docker/Process) | Op launchers | Worker process |
| Observability | Limited (logs + metadata DB) | Built-in UI + events | Asset catalog + lineage | Workflow history |
| Data lineage | External (OpenLineage) | None native | Built-in (Assets) | None native |
| Learning curve | High | Medium | Medium | High |
| Best for | ETL/ELT, well-established pipelines | Python data pipelines, ML | dbt + asset lineage, BI | Long-running workflows, sagas |
Airflow: When to Choose
# Choose Airflow when:
# - Team already knows Airflow, large operator ecosystem
# - Complex DAG dependencies with 100+ tasks
# - Integration with Hadoop/Spark ecosystem
# - Enterprise: Astronomer/Google Composer/MWAA support needed
from airflow import DAG
from airflow.operators.python import PythonOperator
from datetime import datetime
with DAG("etl_orders", start_date=datetime(2024,1,1), schedule="@daily") as dag:
extract = PythonOperator(task_id="extract", python_callable=extract_fn)
transform = PythonOperator(task_id="transform", python_callable=transform_fn)
load = PythonOperator(task_id="load", python_callable=load_fn)
extract >> transform >> load
Prefect: When to Choose
# Choose Prefect when:
# - Python-first workflows without YAML/DAG overhead
# - Dynamic workflows that change structure at runtime
# - Rapid iteration needed (no DAG file structure requirements)
from prefect import flow, task
from prefect.deployments import Deployment
from prefect.server.schemas.schedules import CronSchedule
@task(retries=3, retry_delay_seconds=60)
def extract_orders(ds: str) -> list[dict]:
...
@task
def transform_orders(orders: list[dict]) -> list[dict]:
...
@task
def load_orders(orders: list[dict], ds: str) -> int:
...
@flow(name="etl-orders", log_prints=True)
def etl_orders_flow(ds: str):
orders = extract_orders(ds)
transformed = transform_orders(orders)
count = load_orders(transformed, ds)
return count
# Deploy with schedule
deployment = Deployment.build_from_flow(
flow=etl_orders_flow,
name="daily-orders",
schedule=CronSchedule(cron="0 2 * * *", timezone="UTC"),
work_pool_name="kubernetes-pool",
)
deployment.apply()
Dagster: When to Choose
# Choose Dagster when:
# - Asset-centric thinking (what data exists, not what tasks run)
# - dbt integration is central to the platform
# - Partition-aware incremental processing
# - Data lineage and catalog are first-class requirements
from dagster import asset, AssetIn, DailyPartitionsDefinition
partitions = DailyPartitionsDefinition(start_date="2024-01-01")
@asset(
partitions_def=partitions,
group_name="bronze",
description="Raw orders from source system",
)
def raw_orders(context) -> None:
ds = context.partition_key
extract_and_store(ds, "s3://data-lake/bronze/orders/")
@asset(
ins={"raw_orders": AssetIn()},
partitions_def=partitions,
group_name="silver",
description="Cleaned and deduplicated orders",
)
def orders(context, raw_orders) -> None:
ds = context.partition_key
transform_and_store(ds)
# Dagster UI shows: asset graph, partition status, lineage
# dbt integration: load dbt project as Dagster assets
from dagster_dbt import dbt_assets, DbtProject
dbt_project = DbtProject(project_dir="dbt/")
@dbt_assets(manifest=dbt_project.manifest_path)
def dbt_analytics(context, dbt):
yield from dbt.cli(["build"], context=context).stream()
Temporal: When to Choose
# Choose Temporal when:
# - Long-running workflows (hours to days)
# - Saga pattern: multi-step transactions with compensation
# - Exactly-once execution guarantees required
# - Microservice coordination (not just data pipelines)
from temporalio import activity, workflow
from datetime import timedelta
@activity.defn
async def validate_order(order_id: str) -> bool:
...
@activity.defn
async def charge_payment(order_id: str, amount: float) -> str:
...
@activity.defn
async def ship_order(order_id: str, payment_id: str) -> None:
...
@activity.defn
async def refund_payment(payment_id: str) -> None:
...
@workflow.defn
class OrderFulfillmentWorkflow:
@workflow.run
async def run(self, order_id: str, amount: float) -> str:
is_valid = await workflow.execute_activity(
validate_order,
order_id,
start_to_close_timeout=timedelta(minutes=5),
)
if not is_valid:
raise ValueError("Invalid order")
payment_id = await workflow.execute_activity(
charge_payment,
args=[order_id, amount],
start_to_close_timeout=timedelta(minutes=10),
)
try:
await workflow.execute_activity(
ship_order,
args=[order_id, payment_id],
start_to_close_timeout=timedelta(hours=1),
)
except Exception:
# Compensation: refund on ship failure
await workflow.execute_activity(refund_payment, payment_id)
raise
return payment_id
Event-Driven Triggering Patterns
# Airflow: Dataset-based triggering
from airflow import Dataset
bronze_orders = Dataset("s3://data-lake/bronze/orders/")
silver_orders = Dataset("s3://data-lake/silver/orders/")
# DAG A: produces bronze_orders
with DAG("extract_orders", schedule="0 1 * * *") as dag:
PythonOperator(
task_id="extract",
python_callable=extract_fn,
outlets=[bronze_orders], # marks dataset as updated
)
# DAG B: triggered when bronze_orders is updated
with DAG("transform_orders", schedule=[bronze_orders]) as dag:
PythonOperator(
task_id="transform",
python_callable=transform_fn,
outlets=[silver_orders],
)
# Prefect: event-driven deployments
from prefect.events import DeploymentEventTrigger
# Trigger when S3 event received
Deployment.build_from_flow(
flow=transform_flow,
name="transform-on-upload",
triggers=[
DeploymentEventTrigger(
expect={"prefect.flow-run.Completed"},
match_related={"prefect.resource.name": "extract-orders"},
)
],
)
Migration from Airflow to Dagster
# Migration strategy:
# 1. Run Dagster alongside Airflow (parallel operation)
# 2. Migrate lowest-risk DAGs first
# 3. Use Dagster's Airflow migration utilities
# Dagster migration table
MIGRATION_MAP = {
"airflow_concept": "dagster_equivalent",
"DAG": "Job / Asset graph",
"Task": "Op / Asset",
"TaskInstance": "Op execution",
"XCom": "IO Manager / Output",
"Variable": "Resource / Config",
"Connection": "Resource",
"Pool": "Concurrency limit",
"Sensor": "Sensor / Auto-materialize",
"schedule_interval": "ScheduleDefinition / cron_schedule",
}
# Dagster equivalent of Airflow DAG:
from dagster import job, op, ScheduleDefinition
@op
def extract(): ...
@op
def transform(orders): ...
@op
def load(transformed): ...
@job
def etl_orders():
load(transform(extract()))
daily_schedule = ScheduleDefinition(
job=etl_orders,
cron_schedule="0 2 * * *",
)
Evaluation Scorecard
Score each 1-5 for your use case:
Airflow Prefect Dagster Temporal
Operator ecosystem 5 2 3 1
Python ergonomics 2 5 4 5
Asset lineage 2 2 5 1
Learning curve (lower=easier) 2 4 4 2
Cloud managed option 5 5 4 2
dbt integration 3 3 5 1
Long-running workflows 2 3 3 5
Community/adoption 5 4 4 3
Decision: highest total score for your weighted criteria
Anti-Patterns
- Choosing orchestrator based on hype — evaluate against your actual use cases (number of DAGs, team Python skills, data lineage needs); wrong tool is expensive to migrate from.
- Running all orchestrators simultaneously — Airflow for ETL + Prefect for ML + Dagster for analytics creates operational complexity; standardize on 1-2 max.
- Migrating all DAGs at once — migrate incrementally; start with the 5 lowest-risk DAGs to learn the new system before committing.
- Using Airflow for dynamic/branching logic — complex Airflow DAGs with PythonBranchOperator become unreadable; Prefect's native Python flow is much cleaner for conditional logic.
- Not evaluating managed options — self-hosted Dagster/Airflow requires dedicated DevOps effort; Astronomer/Prefect Cloud/MWAA can be more cost-effective for small teams.
References
- Airflow:
airflow.apache.org - Prefect:
docs.prefect.io - Dagster:
docs.dagster.io - Temporal:
docs.temporal.io - Dagster dbt integration:
docs.dagster.io/integrations/dbt - Related skills:
[[airflow-dags]],[[dagster-assets]],[[prefect-workflows]]