Schedule lineage-aware pipeline runs with agents, all in Python

16,000+ companies already ingest data with dlt in production; teams schedule, backfill, and chain their pipelines with dltHub.

Orchestrating pipelines with dltHub

Your pipeline ops co-pilot. Paste this prompt into Claude/Codex/Cursor:

Run uvx dlthub-start then /setup-runtime to take the dlt pipelines I already run locally and run them as production jobs on dltHub

dltHub

"dltHub Pro lets my agents develop data pipelines locally, test changes quickly and cheaply in CI, and then runs them in the cloud against my largest workloads. It gives them the tools to take care of the knucklehead stuff so that I can get a good night's sleep."

Josh Wills

Josh Wills

Member of Technical Staff, DatologyAI

THE AGENTIC DATA ENGINEERING LIFECYCLE ON DLTHUB

One pipeline through CI, production, and replay

The function you merge is the job. The schedule ships in that PR, not a YAML DAG beside the code. Teams already on Airflow keep Airflow — the dlt adapter maps the same pipeline onto DAGs and data intervals.

Jobs
All jobs defined in this workspace
Job nameTypeProfileStatusLast run
github_eventspipelineanalyticsSuccess2 min ago
stripe_payoutspipelinefinanceRunningnow
hubspot_contactspipelinemarketingSuccess14 min ago
rev_attribution_modeltransformfinanceSuccess1 hr ago
salesforce_objectspipelinesalesQueuedqueued
data_quality_checksverifyplatformSuccess1 hr ago
postgres_orders_cdcpipelinecommerceFailed3 hr ago
snowflake_exportexportanalyticsPaused2 days ago
1 · READY

Workspace, hub extra, logged in

/setup-runtime is the entry skill. It checks pyproject.toml, creates .dlt/.workspace with dlthub init, installs dlt[hub], then dlthub login and dlthub workspace connect. The rest of the toolkit does not run until those pass.

Outcome: a workspace the Runtime CLI can talk to.

dltHub toolkit·dlthub-platform
1 skill
uv run dlthub ai toolkit install dlthub-platform
/setup-runtime

Opus 5.0 · dltHub · ~/agent-observability

>
? for shortcuts
Shell
dlthub init --name my_workspace
uv add "dlt[hub]"
dlthub login
dlthub workspace connect
2 · PREPARE

Prod credentials and the deployment module

/prepare-deployment splits dev and prod secrets into profile TOML, points the production destination, and writes the deployment module. The first line of the __deployment__.py docstring is the workspace description in the dashboard; __all__ is the deploy list.

Outcome: a prod profile and an explicit job list.

dltHub toolkit·dlthub-platform
1 skill
uv run dlthub ai toolkit install dlthub-platform
/prepare-deployment

Opus 5.0 · dltHub · ~/agent-observability

>
? for shortcuts
Python
"""Jaffle Shop -- ingest and transform customer data"""

from jaffle_ingestion import ingest_jaffle
from jaffle_transformations import transform_jaffle

__all__ = ["ingest_jaffle", "transform_jaffle"]
3 · SCHEDULE

Cron, chain, and backfill from the decorator

/deploy-workspace is the skill that writes schedules, followup chains, and refresh cascades, then syncs them. Triggers live in code — dlthub deploy reconciles the manifest, classifying every job as added, updated, unchanged, or archived. Simulate with dlthub local run before burning a cloud run.

Outcome: the job graph is live on Runtime.

dltHub toolkit·dlthub-platform
1 skill
uv run dlthub ai toolkit install dlthub-platform
/deploy-workspace

Opus 5.0 · dltHub · ~/agent-observability

>
? for shortcuts
Python
@run.pipeline("github_pipeline", trigger=trigger.every("5m"))
def load_commits():
    ...


@run.pipeline("jaffle_transform", trigger=ingest_jaffle.success)
def transform_jaffle():
    ...
Shell
dlthub deploy --dry-run
dlthub local run load_commits
dlthub deploy
4 · DEBUG

Status, logs, failed runs

/debug-deployment is the operations skill: fnmatch selectors like tag:ingest and schedule:*, streamed logs, cancel, and diagnosing a red run.

Outcome: a failed run has a log trail.

dltHub toolkit·dlthub-platform
1 skill
uv run dlthub ai toolkit install dlthub-platform
/debug-deployment

Opus 5.0 · dltHub · ~/agent-observability

>
? for shortcuts
Shell
dlthub job list "schedule:*"
dlthub job logs load_commits -f
dlthub job cancel "tag:ingest"
6 of 9

Every run owns a window, and a backfill is one command

Declare an interval and take a run_context parameter, and the scheduler hands each run the exact window it owns. Miss a tick and interval_start extends back to where the last successful run ended, so windows stay continuous and no data is dropped. Pair that with write_disposition="merge" and a primary key and every replay is safe.

A backfill is a signal that propagates through the job graph rather than operational surgery. One job marked refresh="always" clears completion state across everything downstream, each job decides what refresh means for its own data, and the scheduler resets interval pointers back to interval.start — no one edits cursor state by hand.

github_eventsRunning
Source: github (rest_api) · 4 resources · destination bigquery://github_data
Schedule
every 30 min
Success rate
99.4%
Avg duration
4.21s
Last run
just now
Rows loaded · last 24 hours
54,128
↑ 6.2% vs yesterday
Resources
issues1,284
pulls612
comments318
releases0
Recent runs
#4128
13:42 · today · 4.21s
2,214
Running
#4127
13:12 · today · 4.04s
2,189
Success
#4126
12:42 · today · 4.17s
2,202
Success
Python
from dlt.hub.run import TJobRunContext

USGS_EPOCH = datetime(2025, 1, 1, tzinfo=timezone.utc)


@run.job(expose={"tags": ["backfill"]}, refresh="always")
def backfill_usgs():
    """Cascade a refresh signal downstream. Loads no data."""


@run.pipeline(
    usgs_ing_pipeline,
    interval={"start": USGS_EPOCH},                  # where the data begins
    trigger=["*/3 * * * *", backfill_usgs.success],
    require={"timezone": "Europe/Berlin"},
)
def usgs_daily(run_context: TJobRunContext):
    if run_context["refresh"]:
        usgs_ing_pipeline.refresh = "drop_sources"
    _load_ingest(
        run_context["interval_start"],   # supplied by the scheduler
        run_context["interval_end"],
        ["earthquakes", "feeds_summary"],
    )
INTEROPERABLE BY DEFAULT

Already on Airflow? Keep Airflow

Not everyone gets to choose their orchestrator. dlt is just Python, so it runs anywhere Python runs — and after a decade of Airflow's dominance, that is often Airflow. dlt.helpers.airflow_helper.PipelineTasksGroup wraps a dlt pipeline as an Airflow task group and handles the un-fun parts: mapping the pipeline's structure onto tasks, a tenacity retry policy scoped to the resumable load steps instead of blindly re-running the whole pipeline, routing dlt's logs into the Airflow task log, and wiping the local working directory on managed runners like Composer and MWAA.

You don't have to write the DAG by hand either. dlt deploy pipedrive_pipeline.py airflow-composer inspects a working pipeline and generates the DAG pre-wired with PipelineTasksGroup, plus a Cloud Build step that rsyncs the DAG folder to your Composer bucket. It ships with schedule=None, so you flip it to your cron once you've watched a manual run succeed.

Queued
Running
Success
Queued
Python
import dlt
from airflow.decorators import dag
from dlt.common import pendulum
from dlt.helpers.airflow_helper import PipelineTasksGroup
from tenacity import Retrying, stop_after_attempt


@dag(schedule="@daily", start_date=pendulum.DateTime(2026, 1, 1), catchup=False)
def load_pipedrive():
    tasks = PipelineTasksGroup(
        pipeline_name="pipedrive",
        use_data_folder=False,     # True: stage working files in the Composer data bucket
        wipe_local_data=True,      # clean the pipeline working dir after the run
        use_task_logger=True,      # route dlt logs into the Airflow task log
        retry_policy=Retrying(stop=stop_after_attempt(3), reraise=True),
    )

    # import sources INSIDE the dag function — top-level code slows DAG parsing
    from pipedrive import pipedrive_source

    pipeline = dlt.pipeline(
        pipeline_name="pipedrive",
        destination="bigquery",
        dataset_name="pipedrive_data",
    )

    tasks.add_run(
        pipeline=pipeline,
        data=pipedrive_source(),
        decompose="serialize",
        trigger_rule="all_done",
        provide_context=True,
    )


load_pipedrive()
DECOMPOSE

One dlt pipeline, one Airflow task per resource

decompose controls how a dlt source maps onto Airflow's task graph. "none" runs the whole pipeline in a single task, which is right for small sources and the simplest logs. "serialize" turns each resource into a sequential task sharing one pipeline state. "parallel" fans resources out concurrently, running the first task alone to set up schema and state. "parallel-isolated" does the same with a separate pipeline, and isolated state, per task.

Outcome: your dlt pipeline stops being a black box in the Airflow UI. Each resource becomes a task with its own duration, logs, and red or green state.

Jobs
All jobs defined in this workspace
Job nameTypeProfileStatusLast run
github_eventspipelineanalyticsSuccess2 min ago
stripe_payoutspipelinefinanceRunningnow
hubspot_contactspipelinemarketingSuccess14 min ago
rev_attribution_modeltransformfinanceSuccess1 hr ago
salesforce_objectspipelinesalesQueuedqueued
data_quality_checksverifyplatformSuccess1 hr ago
postgres_orders_cdcpipelinecommerceFailed3 hr ago
snowflake_exportexportanalyticsPaused2 days ago
Python
# fan every resource out in parallel
tasks.add_run(pipeline=pipeline, data=source, decompose="parallel", trigger_rule="all_done")

# or orchestrate at resource level: everything on the default cursor,
# but `activities` on its own start date
source = pipedrive_source()
source.resources["activities"].selected = False

activities_source = pipedrive_source(
    since_timestamp="2026-03-01 00:00:00Z"
).with_resources("activities")

tasks.add_run(pipeline=pipeline, data=source, decompose="serialize")
tasks.add_run(pipeline=pipeline, data=activities_source, decompose="serialize")
DATA INTERVALS

Let Airflow drive incremental loading

By default dlt tracks its own incremental cursor in pipeline state, which means state clearing during backfills and cursors that ignore the DAG's data intervals. Set allow_external_schedulers=True and dlt reads data_interval_start and data_interval_end from the task context, binding the incremental to exactly that window. The same resource outside Airflow falls back to normal stateful incremental loading, and you can inject windows in CI with DLT_INTERVAL_START and DLT_INTERVAL_END.

The payoff is that Airflow's own backfill machinery just works: catchup=True generates a run for every missed interval and each one loads precisely its slice, with zero cursor bookkeeping. It is the same "the scheduler owns the window, the source is a function of it" design as Runtime's run_context, expressed through Airflow's native interval model. One caveat: a manually triggered DAG run gets the degenerate interval (now, now) and yields no data — use "Run with config" and set a logical date in the past instead.

Run #4128 · github_events
Pipeline running
1Extractrunning…
2Normalize
3Load
4Verify
Python
@dlt.resource(primary_key="id")
def tickets(
    zendesk_client,
    updated_at=dlt.sources.incremental[int](
        "updated_at",
        allow_external_schedulers=True,   # defer to the DAG's data interval
    ),
):
    for page in zendesk_client.get_pages(
        "/api/v2/incremental/tickets", "tickets",
        start_time=updated_at.start_value,
    ):
        yield page


@dag(
    schedule="@weekly",
    start_date=pendulum.datetime(2024, 1, 1, tz="UTC"),
    catchup=True,          # generate a run for every week since start_date
    max_active_runs=3,     # backfill three weeks at a time
)
def zendesk_backfill():
    ...
Shell
dlt deploy pipedrive_pipeline.py airflow-composer
# or, secrets as env vars instead of an Airflow Variable:
dlt deploy pipedrive_pipeline.py airflow-composer --secrets-format env

Two models, the same problems

dltHub Runtime

Scheduling lives in the pipeline code

Cron and intervals are decorators: trigger.schedule, trigger.every
Run-after-success: trigger=upstream.success
Run-only-if-current: freshness=upstream.is_fresh
Windows arrive as run_context, auto-extended over missed ticks
Backfill is a refresh cascade: refresh="always", --refresh
Per-job dependency_groups and profiles isolate environments
Change a schedule: edit the decorator, dlthub deploy

Airflow + the dlt adapter

Scheduling lives in separate DAG files

Cron and intervals are DAG params: schedule=, timetables
Run-after-success: dataset/asset triggers, TriggerDagRunOperator
Run-only-if-current: sensors such as ExternalTaskSensor
Windows are data intervals + allow_external_schedulers=True
Backfill is catchup=True, airflow dags backfill, clearing runs
Virtualenv or pod images isolate environments
Change a schedule: edit the DAG file, sync to the scheduler

Neither is exclusive. Already running Airflow? Use the adapter and get per-resource observability and interval-correct backfills inside the orchestrator your team already operates. Starting fresh with pipelines as the workload? Runtime removes orchestration as a separate system to run. And dlt goes anywhere Python does — there are equivalent guides for Dagster, Prefect, and Kestra.

Complete agentic workflows for every phase of data engineering

Not autocomplete, not a chatbot on a dashboard. A guided sequence of skills, commands, rules, and MCP - with guardrails agents can't skip. Maintained by dltHub, controlling the infrastructure agents and pipelines operate on.

Start
Quick Startdlt1 skill · 1 cmd · 1 rule · MCP
Initdlt3 skills · 1 cmd · 1 rule · MCP
Ingest
REST API Pipelinedlt8 skills · 1 rule · MCP
SQL Database Pipelinedlt8 skills · 1 rule · MCP
Filesystem Pipelinedlt3 skills · 1 rule · MCP
Harden
Data QualitydltHub4 skills · 2 rules · MCP
Performancedlt1 skill · 1 rule · MCP
Transform
TransformationsdltHub6 skills · 1 rule · MCP
Explore
Data Explorationdlt2 skills · 1 rule
Operate
dltHub PlatformdltHub4 skills · 2 rules

Discover individual skills per agentic workflow

See how each workflow guides your agent - step by step, from first prompt to production deployment.

Quick Start1/1

The guided entry point. Names a use case, checks the workspace, and hands off to the right toolkit in a few prompts.

Opus 5.0 · Quick Start · ~/pipelines

>
? for shortcuts
Ask your agentcopies with harness setup

Take me through the full workflow with the GitHub API

Blueprints for data workflows

dltHub is a composable data platform. Blueprints are its ready-made builds: each one dltHub assembled for a specific use case, end to end, from the sources you already use to a production dashboard or API.

  • Every teamBrowse every dltHub Blueprint

Your coding agent

Claude Code, Cursor or Codex

operates through
dltHubthe agentic layer

dltHub AI harness

Agentic primitives to build, run and fix pipelines

dltHub context catalog

Lineage, schema, data quality, governance, run state

grounds and runs
Trace pipelines
  • Pydantic Logfire

    Pydantic Logfire

  • Arize

    Arize

  • Langfuse

    Langfuse

  • LangChain

    LangChain

dltHubthe managed infrastructure layer

Ingest and standardize traces into the OpenAI messages format as a training-ready dataset.

API
distil labs

distil labs

Fine-tune a specialist model, served as a drop-in replacement via API to distil labs customers.

View the Agent distillation with distil labs blueprint

Frequently Asked Questions

Do I have to replace Airflow to use dltHub?

No. dlt is just Python, so it runs anywhere Python runs. If your team already operates Airflow, the dlt adapter (PipelineTasksGroup) wraps a pipeline as a task group with retry policy, log routing, and a decompose option that turns each resource into its own Airflow task. Set allow_external_schedulers=True and dlt binds its incremental cursor to the DAG's data interval, so catchup backfills load exactly the right slice. dlt deploy ... airflow-composer generates the starting DAG for you. There are equivalent guides for Dagster, Prefect, and Kestra.

How do backfills work on dltHub Runtime?

A backfill is a refresh signal that propagates through the job graph, not a manual pause-clear-rerun. A job declared with refresh="always" originates the signal on every successful run; downstream jobs pass it through by default or stop it with refresh="block". Runtime clears each reachable job's completion state and resets its interval pointer, so the next run reprocesses from the start of its declared interval. Kick one off with dlthub job trigger "tag:backfill".

Why is there no platform-level retry setting?

Retry logic belongs where resumability is known: in the pipeline code. A blanket "retry 3x" above a non-idempotent load is how you double-ingest. dlt itself ships request retries and resumable loads, and execute={"timeout": ...} gives a grace period so a run can flush buffers and commit in-flight loads before a hard kill.

What is dlt?

dlt (data load tool) is an open-source Python library for building data pipelines. It handles schema inference, incremental loading, nested data normalization, and works with 10,100+ sources. Apache 2.0 licensed and always free to use.

What is dltHub?

dltHub is the managed agentic platform for running dlt pipelines in production. It bundles a managed runtime (deploy with one command, no infra to patch), Python and SQL transformations orchestrated inside your pipeline, data quality checks that fail fast with actionable errors, a managed Iceberg lakehouse with the option to bring your own storage, and an MCP server so agents can analyze pipelines and datasets directly. The outcome: teams ship trustworthy data faster, without owning the infrastructure. See the full feature list in the dltHub docs.

How is dltHub different from a Claude skill or tools like Replit?

Tools like Claude skills or Replit are great for writing and running code. But they are not built for data engineering workflows end to end. dltHub gives your team complete agentic workflows that cover every phase: coding, running, deploying, and debugging pipelines, on infrastructure you control.

How do I get access to dltHub?

dltHub is available now. Book a demo with our team to get set up, or see our pricing page for plans and what's included.