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 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
Member of Technical Staff, DatologyAI

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.
| Job name | Type | Profile | Status | Last run |
|---|---|---|---|---|
| github_events | pipeline | analytics | Success | 2 min ago |
| stripe_payouts | pipeline | finance | Running | now |
| hubspot_contacts | pipeline | marketing | Success | 14 min ago |
| rev_attribution_model | transform | finance | Success | 1 hr ago |
| salesforce_objects | pipeline | sales | Queued | queued |
| data_quality_checks | verify | platform | Success | 1 hr ago |
| postgres_orders_cdc | pipeline | commerce | Failed | 3 hr ago |
| snowflake_export | export | analytics | Paused | 2 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.
uv run dlthub ai toolkit install dlthub-platformOpus 5.0 · dltHub · ~/agent-observability
dlthub init --name my_workspace
uv add "dlt[hub]"
dlthub login
dlthub workspace connect2 · 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.
uv run dlthub ai toolkit install dlthub-platformOpus 5.0 · dltHub · ~/agent-observability
"""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.
uv run dlthub ai toolkit install dlthub-platformOpus 5.0 · dltHub · ~/agent-observability
@run.pipeline("github_pipeline", trigger=trigger.every("5m"))
def load_commits():
...
@run.pipeline("jaffle_transform", trigger=ingest_jaffle.success)
def transform_jaffle():
...dlthub deploy --dry-run
dlthub local run load_commits
dlthub deploy4 · 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.
uv run dlthub ai toolkit install dlthub-platformOpus 5.0 · dltHub · ~/agent-observability
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.
| Run | Started | Duration | Rows | Status |
|---|---|---|---|---|
| #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 |
| #4125 | 12:12 · today | 4.31s | 2,176 | Success |
| #4124 | 11:42 · today | 42.5s | 0 | Failed |
| #4123 | 11:12 · today | 4.08s | 2,164 | Success |
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.
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.
| Job name | Type | Profile | Status | Last run |
|---|---|---|---|---|
| github_events | pipeline | analytics | Success | 2 min ago |
| stripe_payouts | pipeline | finance | Running | now |
| hubspot_contacts | pipeline | marketing | Success | 14 min ago |
| rev_attribution_model | transform | finance | Success | 1 hr ago |
| salesforce_objects | pipeline | sales | Queued | queued |
| data_quality_checks | verify | platform | Success | 1 hr ago |
| postgres_orders_cdc | pipeline | commerce | Failed | 3 hr ago |
| snowflake_export | export | analytics | Paused | 2 days ago |
# 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.
@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():
...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 envTwo models, the same problems
dltHub Runtime
Scheduling lives in the pipeline code
Airflow + the dlt adapter
Scheduling lives in separate DAG files
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.
Discover individual skills per agentic workflow
See how each workflow guides your agent - step by step, from first prompt to production deployment.
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
Take me through the full workflow with the GitHub API
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
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.
Your coding agent
Claude Code, Cursor or Codex
the agentic layerdltHub AI harness
Agentic primitives to build, run and fix pipelines
dltHub context catalog
Lineage, schema, data quality, governance, run state
Pydantic Logfire
Arize
Langfuse
LangChain
the managed infrastructure layerIngest and standardize traces into the OpenAI messages format as a training-ready dataset.
distil labs
Fine-tune a specialist model, served as a drop-in replacement via API to distil labs customers.
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.