Ciaren

Scheduling

Scheduling

Ciaren includes a lightweight, in-process scheduler that runs a flow automatically on a cron schedule — no broker, no extra services. It is meant for simple recurring jobs, not heavy orchestration; Ciaren is not an Airflow/dbt replacement.

How it runs

The scheduler is a single asyncio poller started inside the API process (via the FastAPI lifespan). ciaren serve runs the API and the scheduler together. Run the API without it using ciaren serve --no-scheduler (or CIAREN_SCHEDULER_ENABLED=false). The scheduler is always disabled in tests.

Creating a schedule

Click Schedule in the flow editor toolbar (or go to the Schedules page) to open the schedule builder:

New schedule dialog — frequency picker (Minutes/Hourly/Daily/Weekly/Monthly/Custom cron), time of day, timezone, engine, and Enabled toggle

Schedules page — list of saved schedules with cron expressions, next run times, recent run history, and status

The Recent runs column shows each schedule's last five runs as status icons (oldest to newest), so you can spot a flaky or failing schedule at a glance. Click an icon to open that run in the run detail view.

The frequency picker updates its human-readable summary and cron expression live as you switch between presets and Custom (cron). Minutes fires every N minutes (1-59) for sub-hourly cadences; Hourly fires once an hour at a chosen minute past:

Switching the schedule frequency across Minutes, Daily, Weekly, Monthly, and Custom (cron) — the human-readable summary and cron expression update live

In the UI, open a flow (or the Schedules page) and add a schedule with a cron expression and timezone. Over the API:

curl -X POST http://localhost:8055/api/flows/{flow_id}/schedules \
  -H "Content-Type: application/json" \
  -d '{
        "cron": "0 7 * * *",
        "timezone": "America/New_York",
        "engine": "polars",
        "is_enabled": true,
        "catch_up": false,
        "max_retries": 2,
        "retry_delay_seconds": 60
      }'
FieldDefaultDescription
cron— (required)Standard 5-field cron expression
timezoneUTCIANA timezone used to interpret the cron
name / descriptionnullOptional labels
enginenullEngine for scheduled runs; falls back to the server default
is_enabledtrueWhether the schedule is active
catch_upfalseRun slots missed while the server was down
max_retries0Retries for a failed run before giving up to the next slot (max 10)
retry_delay_seconds60Base backoff between retries
run_timeout_secondsnullPer-schedule run timeout override; falls back to RUN_TIMEOUT_SECONDS (0 = no limit)
parametersnullFlow-parameter overrides applied to every run this schedule fires

The UI includes a cron builder so you don't have to hand-write expressions.

Parameterized flows

If the flow declares parameters, the schedule form shows a Parameter values section. Values you set there apply to every run this schedule fires; blanks fall back to each parameter's default — so one flow can back several schedules that differ only by their parameter values.

Lifecycle overview

Scheduler Lifecycle
Cron fires
Poller wakes every 30 s, checks next_run_at
Run executes
Flow runs on the engine; concurrent limit respected
Success
Run recorded; next_run_at advances to next cron slot
Wait for next slot
Failure streak resets
Failed
Streak increments
Retry (if max_retries > 0)
Exponential backoff, capped at 1 h
Auto-disable (after 5 failures)
Re-enable in the UI to clear streak
TriggerSuccessFailedRetryDisabled

How it decides what to run

Schedule.next_run_at (a naive-UTC timestamp) is the single source of truth. Because it's stored in the database, schedules survive restarts without a separate jobstore. The poller wakes up periodically (SCHEDULER_POLL_INTERVAL_SECONDS, default 30s), runs anything due, and computes the next slot.

Scheduled runs are recorded like any other run, tagged with their trigger and schedule_id. Browse them via GET /api/schedules/{id}/runs or GET /api/runs?schedule_id=..., and they open in the normal run detail view.

Reliability behaviors

  • Concurrency & overlap. Concurrency is capped (SCHEDULER_MAX_CONCURRENT_RUNS, default 1), and a schedule skips a new slot if its previous run is still going, so runs never pile up on each other.
  • Catch-up. If the server was down across one or more slots, catch_up decides whether those missed slots run when it comes back (off by default, so you don't get a burst of stale runs).
  • Retries. A failed run retries up to max_retries with exponential backoff (retry_delay_seconds, capped at 1 hour) before falling back to the next cron slot.
  • Auto-disable. After SCHEDULER_MAX_CONSECUTIVE_FAILURES consecutive failed runs (default 5) a schedule is disabled with a disabled_reason. Re-enabling it clears the failure streak.
  • Orphan recovery. On startup, runs left in running (interrupted by a crash) are marked failed — a single process can't resume them.
  • Manual run-now. POST /api/schedules/{id}/run-now triggers an immediate one-off run that stays outside the retry/auto-disable machinery.

Configuration

SettingDefaultDescription
CIAREN_SCHEDULER_ENABLEDtrueMaster on/off switch
CIAREN_SCHEDULER_POLL_INTERVAL_SECONDS30How often the poller wakes
CIAREN_SCHEDULER_MAX_CONCURRENT_RUNS1Max simultaneous scheduled runs
CIAREN_SCHEDULER_MAX_CONSECUTIVE_FAILURES5Failures before auto-disable (0 = never)

Limitations

  • One schedule runs one flow. There are no cross-flow dependencies or DAGs of flows.
  • The scheduler is single-process. For high-availability or distributed scheduling, export the flow's Python and run it under your own orchestrator.

See also