Jaco van der Laan

State-Aware Orchestration in Metadata-Driven Pipelines

18/03/2026  ·  Automation  ·  Data Engineering  ·  Metadata

metadata
‹ ›

When should your pipelines run? Not on a schedule — when there’s new data to process.


State-Aware Orchestration in Metadata-Driven Pipelines

When should your pipelines run? Not on a schedule — when there’s new data to process.


The Problem with Scheduled Pipelines

Hugo Lu’s article on State-Aware Orchestration identifies a fundamental issue: scheduled pipelines waste resources when:

  1. Data doesn’t land on time
  2. No new data has arrived since last run
  3. Models are forgotten but keep running
  4. Full refreshes happen when incremental would suffice

His framing of “5 assumptions that break at scale” is useful:

State-aware orchestration addresses this by tracking what was processed and only running when there’s work to do.


What Is State?

State in this context is metadata about what has been processed:

┌──────────────────────────────────────────────────────────────┐  
│                        STATE EXAMPLES                        │  
├──────────────────────────────────────────────────────────────┤  
│                                                              │  
│  HIGH WATERMARK                                              │  
│  {"customer_table": {"last_id": 45678, "last_ts": "..."}}    │  
│                                                              │  
│  CHECKPOINT                                                  │  
│  {"pipeline": "customer_etl", "position": "offset:12345"}    │  
│                                                              │  
│  MANIFEST                                                    │  
│  {"model": "customer", "hash": "abc123", "modified": false}  │  
│                                                              │  
│  PARTITION STATE                                             │  
│  {"table": "events", "partitions_loaded": ["2024-01", ...]}  │  
│                                                              │  
└──────────────────────────────────────────────────────────────┘

The insight: state has always existed. Kafka offsets, Spark checkpoints, Iceberg snapshots, Delta transaction logs — all are forms of state. dbt’s “state:modified” is just making this explicit for transformations.


State-Aware vs Schedule-Aware

Traditional scheduling:

# Run at 2am regardless of whether data exists  
schedule:  
  cron: "0 2 * * *"  
tasks:  
  - dbt run --select customer_model

State-aware:

# Run when source data has changed  
model:  
  name: customer_model  
  sla: hourly  
  depends_on:  
    - source: crm.customers  
      state: watermark

The orchestrator:

  1. Checks if crm.customers has new data (watermark > last processed)
  2. If yes → run the model
  3. If no → skip (no wasted compute)

How This Relates to MDDE

MDDE already tracks much of the metadata needed for state-aware orchestration:

1. Entity Lineage = Dependency Graph

entity:  
  id: customer_360  
  sources:  
    - entity: crm_customer  
    - entity: erp_customer  
    - entity: web_analytics_customer

This defines what depends on what.
The orchestrator can use this to determine run order.

2. Delivery Properties = SLA Definition

entity:  
  id: customer_360  
  delivery_properties:  
    refresh_frequency: hourly  
    sla: "by 08:00 daily"  
    criticality: business_critical

This is the when it needs to be ready , not when to run it.

3. Source Freshness = State Tracking

source:  
  id: crm_customer  
  freshness:  
    loaded_at_field: _load_timestamp  
    warn_after: 12 hours  
    error_after: 24 hours

This is input state — has new data arrived?

4. Historization Metadata = Output State

The bi-temporal SCD2 pattern already tracks state:

-- What was last processed?  
SELECT MAX(load_dts) FROM customer_scd2;

-- What's the current watermark?  
SELECT MAX(valid_from) FROM customer_scd2 WHERE is_current = TRUE;

Adding State-Aware Orchestration to MDDE

New: Pipeline State Entity

entity:  
  id: pipeline_state  
  stereotype: operational_metadata  
  description: Tracks processing state for each entity  
  attributes:  
    - name: entity_id  
      type: string  
      description: The entity being tracked  
    - name: last_source_watermark  
      type: timestamp  
      description: Highest watermark seen in source  
    - name: last_processed_watermark  
      type: timestamp  
      description: Highest watermark actually processed  
    - name: last_run_timestamp  
      type: timestamp  
      description: When the pipeline last ran  
    - name: last_run_status  
      type: string  
      description: SUCCESS, FAILED, SKIPPED  
    - name: rows_processed  
      type: bigint  
      description: Number of rows in last run  
    - name: model_hash  
      type: string  
      description: Hash of model definition (detect schema changes)

New: SLA-Based Scheduling

# Instead of cron schedules, define SLAs  
entity:  
  id: customer_360  
  orchestration:  
    sla:  
      - name: hourly_refresh  
        deadline: "+1 hour"  # Ready within 1 hour of source data  
  triggers:  
      - type: source_change  
        sources: [crm_customer, erp_customer]  
      - type: schedule  
        cron: "0 * * * *"  # Check hourly, but skip if no new data  
    skip_conditions:  
      - no_source_changes  
      - within_sla  # Already met SLA, don't re-run

New: Watermark Configuration

source:  
  id: crm_customer  
  watermark:  
    type: timestamp  
    column: updated_at

    # Or for systems without timestamps  
    # type: hash  
    # columns: [customer_id, customer_name, email]

The State Check Pattern

Before running any pipeline:

-- 1. Get current source watermark  
WITH source_state AS (  
    SELECT MAX(updated_at) AS current_watermark  
    FROM source.crm_customers  
),  
-- 2. Get last processed watermark  
processed_state AS (  
    SELECT last_processed_watermark  
    FROM pipeline_state  
    WHERE entity_id = 'customer_360'  
),  
-- 3. Determine if run is needed  
run_decision AS (  
    SELECT  
        CASE  
            WHEN source_state.current_watermark > processed_state.last_processed_watermark  
            THEN TRUE  
            ELSE FALSE  
        END AS should_run,  
        source_state.current_watermark AS new_watermark  
    FROM source_state, processed_state  
)  
SELECT * FROM run_decision;

If should_run = FALSE, skip the pipeline entirely.


Integration with Bi-Temporal SCD2

The SCD2 pattern naturally supports state tracking:

SOURCE DATA                    STATE                      TARGET SCD2  
┌─────────────────┐     ┌─────────────────┐      ┌─────────────────────┐  
│ id: 1           │     │ watermark:      │      │ id: 1               │  
│ name: "Acme"    │     │   2024-03-01    │      │ name: "Acme"        │  
│ updated_at:     │     │                 │      │ valid_from: 2024-01 │  
│   2024-03-01    │     │ last_run:       │      │ valid_to: 9999-12   │  
│                 │     │   2024-03-01    │      │ load_dts: 2024-03   │  
└─────────────────┘     │   06:00:00      │      └─────────────────────┘  
                        └─────────────────┘

No new data scenario:

 Source watermark: 2024-03-01  
Last processed:   2024-03-01  
Decision:         SKIP (no new data)

New data scenario:

 Source watermark: 2024-03-02  
Last processed:   2024-03-01  
Decision:         RUN (process 2024-03-02 data)

State-Aware vs End-to-End Orchestration

Hugo makes an important point: end-to-end orchestration often eliminates the need for state tracking.

WITH END-TO-END ORCHESTRATION:

Source System ──► Ingestion ──► Transformation ──► Delivery  
      │              │               │               │  
      └──────────────┴───────────────┴───────────────┘  
                    Single Pipeline  
If ingestion finds no data → entire pipeline stops

No need for state checks downstream

This is the pattern MDDE already supports with unified pipeline definitions:

pipeline:  
  id: customer_etl

stages:  
    - id: ingest  
      source: crm_api  
      target: raw.crm_customers  
      on_no_data: stop  # Pipeline ends here if no data

    - id: historize  
      source: raw.crm_customers  
      target: source_hist.crm_customers  
      depends_on: [ingest]

    - id: integrate  
      source: source_hist.crm_customers  
      target: integration.customer  
      depends_on: [historize]

When to add state-aware orchestration on top:


The IDS Pattern and State

The IDS (Integrated Data Sets) pattern from our previous article has natural state boundaries:

DELIVERY ENTITY A (IDS)         DELIVERY ENTITY B (IDS)  
     │                               │  
     │ State: watermark_a            │ State: watermark_b  
     ▼                               ▼  
┌─────────────────────────────────────────────────────────────┐  
│                 UNIFIED TABLE                               │  
│                                                             │  
│  PARTITION: entity_a, SOURCE_IDS                            │  
│    └─ Last processed: 2024-03-01 06:00                      │  
│                                                             │  
│  PARTITION: entity_b, SOURCE_IDS                            │  
│    └─ Last processed: 2024-03-01 08:00                      │  
│                                                             │  
│  PARTITION: entity_a, ENRICHMENT                            │  
│    └─ Last processed: 2024-03-01 07:00                      │  
│                                                             │  
└─────────────────────────────────────────────────────────────┘

Each partition can have independent state , enabling:

  • Entity A updates without affecting Entity B
  • Enrichment runs only when base IDS changes
  • Fine-grained watermark tracking per source

Configuration: State-Aware Pipeline

# orchestration/customer_pipeline.yaml  
pipeline:  
  id: customer_pipeline

  state_tracking:  
    enabled: true  
    storage: pipeline_state_table  # Or redis, file, etc.

  sources:  
    - id: crm_customer  
      watermark:  
        column: updated_at  
        type: timestamp  
    - id: erp_customer  
      watermark:  
        column: modification_timestamp  
        type: timestamp

  targets:  
    - id: customer_360  
      partition_by: [delivery_entity_id, source_type]

  orchestration:  
    mode: state_aware  # vs 'scheduled' or 'triggered'  
    check_interval: "*/15 * * * *"  # Check every 15 min  
    run_conditions:  
      - any_source_has_new_data  
      - model_definition_changed  
      - manual_trigger  
    skip_conditions:  
      - all_sources_unchanged  
      - target_fresher_than_sla

  sla:  
    deadline: "08:00 daily"  
    alert_on_miss: true

State Storage Options

MDDE approach: store state as a regular entity with the same patterns as business data:

entity:  
  id: pipeline_state  
  stereotype: operational_metadata  
  storage:  
    type: delta_table  
    location: metadata.pipeline_state

This means state is:

  • Versioned (SCD2 if needed)
  • Queryable
  • Part of the same governance framework
  • Backed up with other metadata

Impact-Driven Refresh Integration

State-aware orchestration connects to MDDE’s impact-driven refresh pattern:

# From ADR-275: Impact Analysis  
impact_refresh:  
  entity_id: customer_360

# Only refresh if impacted sources have new data  
  impact_sources:  
    - crm_customer  
    - erp_customer  
  # Check watermarks before running  
  state_check:  
    - source: crm_customer  
      watermark: updated_at  
    - source: erp_customer  
      watermark: modification_timestamp

  # Skip refresh if no impact  
  skip_if_no_impact: true

Key Takeaways

  1. State-aware orchestration tracks what was processed to avoid redundant runs
  2. MDDE already has the metadata needed (lineage, delivery properties, freshness)
  3. Bi-temporal SCD2 naturally supports watermark tracking via load_dts
  4. End-to-end orchestration often eliminates state needs for simple pipelines
  5. State as an entity makes it part of the governed metadata framework
  6. IDS + unified tables enable per-partition state tracking
  7. SLA-based scheduling is more declarative than cron expressions

The goal: pipelines that run when needed, not when scheduled.


This article is part of the Model-Driven Data Engineering series. Inspired by Hugo Lu’s State-Aware Orchestration article.