Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Schedule Pipelines with Dagster

By Kokil Thapa | Last reviewed: September 2026

You need to schedule pipelines with Dagster when batch jobs must run on a clock, not on a developer laptop. Raw Linux cron works for one script. It breaks down once you need run history, dependency graphs, retries, and safe backfills. Dagster treats schedules as first-class code assets tied to jobs and ops. This guide walks through defining cron schedules, running the daemon, and choosing sensors when events beat the clock. If you already orchestrate with Apache Airflow for data pipelines, the mental model is familiar—but Dagster keeps schedules beside the job definitions you already version in Git.

What do you need before you schedule pipelines with Dagster?

Dagster is a Python orchestrator. Your pipeline code lives in a Dagster project—typically a definitions.py or module the workspace loads. You need Python 3.9+ (3.11 or 3.12 is common on new projects in 2026), Dagster installed, and a job you want to run on a timer.

On a real client project, I treat scheduling like any production cron replacement: version the schedule in Git, run it through the same deploy path as the app, and log every tick. That mirrors how I wire Linux cron jobs for Laravel queues—but Dagster adds a UI, run storage, and lineage.

Minimal project layout

my_dagster_project/
├── pyproject.toml
├── my_dagster_project/
│   ├── __init__.py
│   ├── assets.py          # @asset definitions
│   ├── jobs.py            # define_asset_job(...)
│   ├── schedules.py       # ScheduleDefinition / @schedule
│   └── definitions.py     # Definitions(..., schedules=[...])
└── workspace.yaml

Install Dagster with pip or uv:

pip install dagster dagster-webserver dagster-graphql

Local dev uses two processes: the web UI and the daemon that evaluates schedules and sensors.

Schedule Pipelines with Dagster — Core FlowGit Repojobs + schedulesDagster Daemontick evaluationSchedulecron triggerJob Runops executeRun Storagehistory + logsDagster UIenable / backfillSensorsevent triggersDaemon must run 24/7 in production — schedules do nothing without itCommon mistake: UI running but daemon stopped after deploy
How Dagster connects versioned schedule code, the daemon, and job runs when you schedule pipelines with Dagster
  1. Create a job from assets or ops.
  2. Attach a schedule with a cron string or partition logic.
  3. Register both in Definitions.
  4. Start dagster-daemon run alongside the webserver.
  5. Enable the schedule (stopped by default in many setups).

For JSON config payloads passed between ops, validate shapes early with a JSON formatter during development. Bad config at 2 AM is worse than a failed unit test at 2 PM.

How do you define a cron schedule for a Dagster job?

Dagster accepts standard five-field cron strings (minute, hour, day of month, month, day of week). Use UTC in production unless you document timezone overrides clearly. Nepal teams often schedule around NPT (UTC+5:45)—convert carefully or use Dagster's timezone-aware scheduling options in your deployment config.

ScheduleDefinition example

# schedules.py
from dagster import ScheduleDefinition, DefaultScheduleStatus, define_asset_job
from .assets import daily_sales_assets

daily_sales_job = define_asset_job(
    name="daily_sales_job",
    selection=[daily_sales_assets],
)

daily_sales_schedule = ScheduleDefinition(
    job=daily_sales_job,
    cron_schedule="0 2 * * *",  # 02:00 UTC daily
    name="daily_sales_schedule",
    default_status=DefaultScheduleStatus.STOPPED,
    tags={"team": "analytics", "env": "prod"},
)

Decorator style with run config

from dagster import schedule, RunRequest, SkipReason
import datetime

@schedule(cron_schedule="30 6 * * 1-5", job=daily_sales_job)
def weekday_morning_schedule(context):
    # Skip Nepal public holidays manually or via external calendar
    if context.scheduled_execution_time.weekday() >= 5:
        return SkipReason("Weekend")
    return RunRequest(
        run_key=f"weekday-{context.scheduled_execution_time:%Y-%m-%d}",
        tags={"scheduled": "true"},
        run_config={
            "ops": {
                "load_raw_sales": {
                    "config": {"source_date": context.scheduled_execution_time.strftime("%Y-%m-%d")}
                }
            }
        },
    )

Register schedules in your entry point:

# definitions.py
from dagster import Definitions
from .jobs import daily_sales_job
from .schedules import daily_sales_schedule, weekday_morning_schedule
from .assets import daily_sales_assets

defs = Definitions(
    assets=[daily_sales_assets],
    jobs=[daily_sales_job],
    schedules=[daily_sales_schedule, weekday_morning_schedule],
)

Point workspace.yaml at that module:

load_from:
  - python_module: my_dagster_project.definitions

Start locally:

dagster dev -m my_dagster_project.definitions

The dev command runs webserver and daemon together. That convenience hides a production requirement: the daemon is a separate long-lived service. I've seen the same class of bug on GitLab CI deploy pipelines—the app ships, but the worker process never restarts.

Schedules vs sensors vs manual launches

Trigger typeBest forRuns whenOps burden
Cron scheduleDaily ETL, weekly reportsFixed clock tickLow—set and monitor
SensorNew S3 file, DB row, queue messageExternal eventMedium—cursor state
Manual / APIAd hoc backfills, debuggingHuman or CI callHigh per run
Asset sensorDownstream refresh on upstream materializationLineage eventLow after setup

Official reference: Dagster's schedule docs at docs.dagster.io/concepts/partitions-schedules-sensors/schedules.

How does the Dagster daemon execute scheduled pipeline runs?

The daemon is the heartbeat. It reads schedule definitions, compares cron ticks to the last launch time, and submits runs to your run launcher. Without it, schedules appear in the UI but never fire. This is the single most common production misconfiguration.

Production daemon setup

# systemd unit example — /etc/systemd/system/dagster-daemon.service
[Unit]
Description=Dagster Daemon
After=network.target

[Service]
User=dagster
WorkingDirectory=/opt/dagster/app
Environment=DAGSTER_HOME=/opt/dagster/dagster_home
ExecStart=/opt/dagster/venv/bin/dagster-daemon run
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

Set DAGSTER_HOME to a persistent directory holding dagster.yaml and storage config. On Ubuntu servers I maintain for clients, I follow the same discipline as Linux system administration work: dedicated service user, log rotation, and health checks.

Daemon Schedule Tick Sequence1. Tickcron match2. Evaluateschedule fn3. LaunchRunRequest4. Executerun worker5. LogRun launcher optionsDefault (same host)DockerRunLauncherK8sRunLauncherDagster Clouddagster.yaml selects launcher — must match infraDaemon down = missed ticks (usually not auto-replayed)
Each cron tick flows through evaluation and launch before ops run—core to schedule pipelines with Dagster reliably

Configure run storage and compute in dagster.yaml:

# $DAGSTER_HOME/dagster.yaml
storage:
  postgres:
    postgres_db:
      username: dagster
      password: {env: DAGSTER_PG_PASSWORD}
      hostname: localhost
      db_name: dagster
      port: 5432

run_launcher:
  module: dagster.core.launcher
  class: DefaultRunLauncher

run_coordinator:
  module: dagster.core.run_coordinator
  class: QueuedRunCoordinator
  config:
    max_concurrent_runs: 5

PostgreSQL backs run history for teams outgrowing SQLite. The pattern parallels test data management for pipelines—isolate environments and never point staging schedules at production databases.

Daemon reference: docs.dagster.io/deployment/dagster-daemon.

When should you use partitioned schedules instead of a simple cron?

Simple cron runs one job instance per tick. Partitioned schedules run one slice per tick—ideal for daily date partitions, per-region loads, or incremental backfills.

Daily partitions example

from dagster import DailyPartitionsDefinition, build_schedule_from_partitioned_job

daily_partitions = DailyPartitionsDefinition(start_date="2025-01-01")

partitioned_job = define_asset_job(
    name="partitioned_etl_job",
    selection=[daily_sales_assets],
    partitions_def=daily_partitions,
)

daily_partition_schedule = build_schedule_from_partitioned_job(
    job=partitioned_job,
    hour_of_day=3,
    minute_of_hour=0,
)

Each tick materializes the partition matching the schedule date. You can backfill a date range from the UI without writing one-off scripts. That saves hours when reconciling late-arriving data.

For ML feature stores or embedding indexes, partitioned schedules pair well with patterns from building an embeddings pipeline. Each day gets its own reproducible run key.

Simple Cron vs Partitioned ScheduleSimple cronOne run per tickFull dataset each timeHard to backfill rangesGood: small nightly jobsPartitionedOne slice per tickIdempotent per date keyUI backfill 2025-03-01 → 03-07Good: large incremental ETLPartition backfill workflowMar 1Mar 2Mar 3Mar 4Missed days? Select range in UI — no custom bash loop
Partitioned schedules simplify backfills when you schedule pipelines with Dagster for date-based data

How do sensors complement scheduled Dagster pipelines?

Schedules answer "run at 3 AM." Sensors answer "run when something changed." A sensor polls an external system—S3 prefix, Kafka topic, or upstream table—and yields RunRequest objects when criteria match.

from dagster import sensor, RunRequest, SkipReason
import boto3

@sensor(job=daily_sales_job, minimum_interval_seconds=300)
def s3_file_sensor(context):
    s3 = boto3.client("s3")
    prefix = "landing/sales/"
    resp = s3.list_objects_v2(Bucket="analytics-raw", Prefix=prefix, MaxKeys=1)
    if resp.get("KeyCount", 0) == 0:
        return SkipReason("No new files")
    return RunRequest(run_key=f"s3-{context.cursor or 'init'}")

Sensors also need the daemon. Set minimum_interval_seconds to avoid hammering APIs. Store cursor state in the sensor context so you do not double-process files.

Combine both: a nightly partition schedule for baseline loads, plus a sensor for intraday refreshes when vendors drop files early. That hybrid shows up in analytics platforms and in AI integration and automation flows where embeddings refresh after document uploads.

What production practices keep scheduled Dagster pipelines reliable?

Scheduling is operations, not just Python. Treat missed ticks, stale secrets, and run queue saturation as normal failure modes.

Secrets and config

Never hard-code database passwords in schedule run config. Use environment variables resolved at launch, or a secrets backend your platform supports. The same rules apply as when you manage secrets safely in pipelines or handle secrets in CI/CD.

Idempotency and run keys

Pass a stable run_key from schedules so Dagster deduplicates accidental double launches. Design ops to tolerate retries—upserts beat blind inserts. This mirrors idempotent deploy steps in build pipeline automation best practices.

Monitoring checklist

  • Daemon process alive (systemd or k8s liveness probe).
  • Schedule status RUNNING in UI—not STOPPED after redeploy.
  • Alert on run failures via Slack/email integration or Dagster Cloud alerts.
  • Track duration trends; sudden spikes often mean bad upstream data.
  • Keep max_concurrent_runs below database connection limits.

For Kubernetes deployments, run coordinators and launchers resemble patterns in Kubeflow ML pipelines on Kubernetes—one control plane, many workers. Dagster Cloud removes daemon hosting if your team prefers managed ops (~USD 100–500/month depending on scale; budget Rs 13,000–65,000/month for Nepal teams comparing build-vs-buy).

Production Gotchas — Schedule Pipelines with DagsterDaemon stoppedSchedules silently idleFix: systemd Restart=alwaysSchedule STOPPEDDefault after fresh deployFix: RUNNING in UI or codeUTC vs local timeJobs fire at wrong hourFix: document timezoneNo run_keyDuplicate runs on retryFix: stable dedupe keyHealthy setupDaemon + RUNNING schedule + alerts + idempotent ops
Four recurring production mistakes when teams schedule pipelines with Dagster—and the fixes that stick

Database migration steps should never run inside unreviewed schedule code without the safeguards you'd use for database migrations in CI/CD pipelines. Promote schedule changes through dev → staging → prod like any other code path described in infrastructure promotion pipelines.

On booking and reporting systems I've shipped—such as Adventure Third Pole Trek—nightly aggregation jobs must survive deploys and holidays. Dagster schedules give operators a visible switch and a replay button. That beats grep-ing cron on three servers.

If your team needs a custom orchestration layer around web apps and data jobs, see custom software development or ongoing support and maintenance. For broader CI context, compare GitHub Actions vs Azure Pipelines and Argo Workflows for CI pipelines.

Key Takeaways

  • Define schedules in code beside jobs; register them in Definitions and load via workspace.yaml.
  • Run dagster-daemon run as a persistent service—schedules never fire without it.
  • Use partitioned schedules for date-sliced ETL and one-click backfills from the UI.
  • Add sensors when files or events should trigger runs between cron ticks.
  • Set stable run_key values, externalize secrets, and alert on failed runs.
  • Enable schedules explicitly after deploy; default status is often STOPPED.

People Also Ask

Does Dagster use cron syntax for schedules?

Yes. Dagster schedules accept standard five-field cron strings. You can also use the @schedule decorator to return RunRequest, SkipReason, or multiple requests per tick for advanced control.

What is the difference between Dagster schedules and sensors?

Schedules are time-driven—they fire on cron ticks evaluated by the daemon. Sensors are event-driven—they poll external systems or watch asset materializations and launch runs when conditions match.

Can Dagster schedules run in Docker or Kubernetes?

Yes. The daemon submits runs to the configured run launcher. Docker and Kubernetes launchers spin isolated containers or pods per run while the daemon stays lightweight on a control node.

How do you backfill missed days in Dagster?

Use partitioned schedules and select a date range in the Dagster UI to launch historical partitions. For non-partitioned jobs, trigger manual runs with explicit run config for each date you need to reprocess.

Ship reliable scheduled pipelines

You schedule pipelines with Dagster by codifying cron triggers, keeping the daemon alive, and choosing partitions or sensors based on how your data arrives—not how demos look in tutorials. Start with one job, one schedule, and PostgreSQL run storage before you add ten sensors and wonder why the queue backs up.

Need help designing data jobs beside your Laravel or eCommerce platform—or hardening production orchestration? Contact us to talk through architecture, or browse the blog for more pipeline guides. You can also read about the author on About Me and review shipped work in the portfolio.

Frequently Asked Questions

You need a Dagster Python project with pipeline code the workspace loads, typically through definitions.py and workspace.yaml. Install Dagster with pip or uv, including dagster-webserver and dagster-graphql for the UI. Python 3.9 or higher is required; 3.11 or 3.12 is common on new projects in 2026. You also need at least one job built from assets or ops that the schedule will trigger. Treat schedules like production cron: version them in Git and deploy through the same path as your application code so every tick is logged and auditable.

Attach a ScheduleDefinition or @schedule decorator to your job with a standard five-field cron string covering minute, hour, day of month, month, and day of week. Register the schedule in Definitions alongside your jobs and assets, then point workspace.yaml at that module. Use UTC in production unless you document timezone overrides clearly; Nepal teams on NPT (UTC+5:45) should convert carefully or use Dagster's timezone-aware scheduling in deployment config. The @schedule decorator can return RunRequest with run config, SkipReason for holidays or weekends, or multiple requests per tick for advanced control.

Yes. Dagster accepts standard five-field cron strings. The @schedule decorator adds RunRequest, SkipReason, or multiple requests per tick when you need logic beyond a fixed clock trigger.

The daemon is a long-lived process that reads schedule definitions, compares cron ticks to the last launch time, and submits runs to your run launcher. Without dagster-daemon run, schedules appear in the UI but never fire. This is the single most common production misconfiguration. Locally, dagster dev runs webserver and daemon together, which hides the production requirement that the daemon must run as a separate persistent service, typically under systemd on Ubuntu with Restart=always and a dedicated dagster user.

Schedules are time-driven. The daemon evaluates cron ticks and launches runs on a fixed clock, which suits daily ETL, weekly reports, and other predictable batch work with low ongoing ops burden. Sensors are event-driven. They poll external systems such as S3 prefixes, Kafka topics, or database tables, or watch upstream asset materializations, and yield RunRequest objects when conditions match. Sensors also require the daemon and need cursor state to avoid double-processing. Many production setups combine a nightly partition schedule with a sensor for intraday refreshes when files arrive early.

On each heartbeat, the daemon reads registered schedule definitions, determines whether a cron tick is due based on the last launch time, evaluates any @schedule logic including SkipReason returns, and submits eligible runs through your configured run launcher. Runs flow through a run coordinator such as QueuedRunCoordinator before ops execute. Configure DAGSTER_HOME to a persistent directory holding dagster.yaml with run storage, typically PostgreSQL for teams outgrowing SQLite, and set max_concurrent_runs below your database connection limits. The daemon itself stays lightweight while launchers handle compute isolation.

The daemon is almost certainly not running or not restarted after deploy. Schedules are evaluated only by dagster-daemon run, not by the webserver alone. I've seen the same class of bug on GitLab CI deploy pipelines: application code ships, but the worker process never restarts. After every deploy, confirm the systemd unit or container for the daemon is alive, check schedule status is RUNNING rather than STOPPED, and verify DAGSTER_HOME points to the correct persistent storage. DefaultScheduleStatus.STOPPED means many schedules need explicit enabling after redeploy.

Simple cron runs one job instance per tick. Partitioned schedules run one slice per tick, which fits daily date partitions, per-region loads, and incremental backfills. Define partitions with DailyPartitionsDefinition, attach them to your job, and use build_schedule_from_partitioned_job to set hour and minute. Each tick materializes the partition matching the schedule date. You can backfill a date range from the Dagster UI without one-off scripts, saving hours when reconciling late-arriving data. Partitioned schedules also pair well with ML feature stores where each day needs its own reproducible run key.

For partitioned jobs, open the Dagster UI, select the partitioned schedule or job, and launch a date range to materialize historical partitions in one action. Each partition gets its own reproducible run key tied to that date slice. For non-partitioned jobs, trigger manual runs with explicit run config passing the source_date or equivalent parameter for each date you need to reprocess. Partitioned schedules exist specifically to avoid writing backfill scripts; if you expect regular reconciliation of late data, design partitions from the start rather than retrofitting after months of simple cron runs.

Yes. The daemon submits runs to whatever run launcher you configure in dagster.yaml. Docker and Kubernetes launchers spin isolated containers or pods per run while the daemon stays on a lightweight control node. Run coordinators like QueuedRunCoordinator manage concurrency across those workers. For Kubernetes deployments, the control-plane and worker pattern resembles Kubeflow ML pipelines: one daemon evaluating schedules, many ephemeral workers executing ops. Ensure the daemon itself has liveness probes or systemd restart policies independent of individual run pods, because a crashed worker does not replace a dead scheduler.

Schedules answer run at 3 AM; sensors answer run when something changed. A sensor polls S3, a queue, or an upstream table on minimum_interval_seconds and yields RunRequest when criteria match, or SkipReason when nothing is new. Store cursor state in the sensor context to avoid reprocessing files. Set minimum_interval_seconds high enough to avoid hammering external APIs. A practical hybrid is a nightly partitioned schedule for baseline loads plus an S3 sensor for intraday refreshes when vendors drop files early. Both schedules and sensors require the daemon, so one systemd service covers both trigger types.

Treat scheduling as operations, not just Python. Never hard-code database passwords in schedule run config; use environment variables or a secrets backend. Pass stable run_key values from schedules so Dagster deduplicates accidental double launches, and design ops for idempotency with upserts instead of blind inserts. Monitor daemon process health, confirm schedule status is RUNNING after redeploy, alert on run failures via Slack or Dagster Cloud, and track duration trends for upstream data issues. Promote schedule changes through dev, staging, and prod like any other code. Keep max_concurrent_runs below database connection limits to prevent queue saturation.

Dagster Cloud removes self-hosted daemon ops at roughly USD 100 to 500 per month depending on scale. Budget Rs 13,000 to 65,000 per month if your Nepal team is comparing build versus buy against running dagster-daemon on your own Ubuntu server with PostgreSQL run storage.

Start dagster dev -m my_dagster_project.definitions, which runs the webserver and daemon together in one command. Point workspace.yaml at your definitions module with load_from and a python_module entry. Define schedules in schedules.py, register them in Definitions, and enable the schedule in the UI since default_status is often STOPPED. Validate run config shapes early during development; bad config at 2 AM is worse than a failed unit test at 2 PM. Use a JSON formatter to catch malformed payloads between ops before they reach a scheduled tick in production.

Raw Linux cron works for one script. It breaks down once you need run history, dependency graphs, retries, and safe backfills. Dagster treats schedules as first-class code assets tied to jobs and ops, versioned beside definitions in Git with a UI, run storage, and lineage. Operators get a visible enable switch and a replay button instead of grepping crontab across three servers. The trade-off is operational overhead: you must keep dagster-daemon run alive as a persistent service and configure run storage, whereas cron needs only a single line in crontab. For analytics ETL with partitions and backfills, Dagster wins; for a lone nightly script, cron may still suffice.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: