Export Scheduling & Timing Guide
Audience: CS, SE, and Engineering teams
Last verified against codebase: Feb 2026
Widget Exports
Widget exports are scheduled via a Kubernetes CronJob that runs every hour (0 * * * *). This triggers the Airflow widget-exporter DAG, which runs the export:widget[company_id] rake task as a K8s pod. Within each run, CustomScheduler.time_to_run? determines which configs are due.
How Scheduling Works Per Config
Each WidgetExportConfig has two key fields in its metadata JSON:
Field | Type | Default | Description |
|---|---|---|---|
| Integer (0-23) | 0 | Hour to run, in the company's timezone |
| Integer (1-24) | 24 | Hours between runs |
CustomScheduler evaluates whether a config is due based on:
The config's
export_hour(in company timezone, NOT UTC)The config's
interval(default 24h, but configurable down to 1h)The config's
last_job_started_attimestamp
Important: There are no fixed "3 AM / 11 AM UTC" slots. Those were legacy values from before the migration to per-config scheduling. Each config runs at its own export_hour in the company's timezone.
Historical Context
The old system used an export_time field with values "morning" (3 AM UTC) and "evening" (11 AM UTC). A migration script (scripts/widget_export_configs_migration.rb) converted these to timezone-relative export_hour values. The old export_time field still exists in the schema with a TODO to remove it.
How to Check a Customer's Widget Export Schedule
Look up the
WidgetExportConfigin Retool (Admin > Widget Export Configs)Check the
metadataJSON forexport_hourandintervalLook up the company's timezone to convert
export_hourto UTCIf
export_houris missing, it defaults to 0 (midnight in company timezone)If
intervalis missing, it defaults to 24 (once daily)
Manual Runs
Manual / one-off widget exports use a separate path from the scheduled pipeline:
Triggered via Retool, which calls the
schedule_exportcontroller actionThis enqueues a
WidgetExportJobon the Sidekiqwidget_exporterqueueThe job creates a temporary (non-persisted)
WidgetExportConfigfromtemp_config_paramsA dedicated Sidekiq deployment (
sidekiq-widget-exporter-deployment.yaml) processes this queue
Key distinction: Scheduled exports run via Airflow/K8s pods (rake task). Manual exports run via Sidekiq. They are different execution paths.
Data Exports
Data exports are currently scheduled via cron jobs defined in cron_job/jobs/production/data_exporter_jobs.rb.
Standard Schedule (Non-DH Customers)
Type | Cron | Time (UTC) | What it does |
|---|---|---|---|
Primary |
| 2:00 PM UTC | Full duration export for each channel |
Secondary |
| 12:00 AM UTC (midnight) | Last 3 days lookback ( |
DH-Specific Schedule
DH (Dentsu Happiness) has custom timing due to their data pipeline dependencies:
Type | Cron | Time (UTC) |
|---|---|---|
DH Primary |
| 9:30 AM UTC |
DH Secondary |
| 4:30 PM UTC |
Why DH is different: After the Mozek migration, DH exports needed to align with their internal data refresh cadence. This was set via tenant-level override (see Shortcut sc-53018).
Resource Allocation
Data export pods have different memory allocations based on channel size:
Tier | Channels | Memory |
|---|---|---|
HIGH | pod | 10240 Mi |
MEDIUM | facebook, adwords | 5120 Mi |
DEFAULT | All others | 3072 Mi |
Primary vs Secondary Exports
Primary: Exports the full configured duration (e.g.,
last_30d,last_90d). Runs once daily.Secondary: Exports only the last 3 days. Useful for downstream systems that need fresher data without waiting for the full primary run.
Upcoming: Mozek Migration for Data Exports (sc-52754)
Status: In development (branch sc-52754/move-data-exporter-crons-to-mozek, not yet merged). Author: Chinmay Relkar.
What's Changing
Data export scheduling is being moved from adwyze cron jobs into Mozek's DAG-based orchestration framework. This adds data exports as a first-class workload type in Mozek, alongside ad channel data pipelines.
New Mozek Tenant Types
Two new tenant types have been added to Mozek.V3.Orchestrator.Tenant:
Tenant Type | ID | Purpose |
|---|---|---|
| 5 | Primary data exports (full duration) |
| 6 | Secondary data exports (last 3 days) |
New DAG Definitions
Two single-node DAGs have been created:
data_export— triggers primary export via Sidekiq executordata_export_secondary— triggers secondary export via Sidekiq executor
Both DAGs use dynamic split args to resolve tenant_external_id from the tenant's external ID, and both set tenant_source: "clarisights".
Failure Recovery
Both DAGs are configured with:
Retry window: 5 days (432,000 seconds)
Base retry interval: 10 minutes (600 seconds)
This is a major improvement over the current cron-based approach, which has no built-in retry or failure recovery.
What This Means for the Team
Once merged, this migration brings several benefits:
Automatic failure recovery — failed exports will be retried within the 5-day window, instead of waiting for the next day's cron
Non-overlapping work guarantee — Mozek prevents duplicate export runs
Granular checkpointing — lower cost of failure vs fire-and-forget crons
Visibility & alerts — export status visible in Mozek dashboard
Automatic data gap fixing — Mozek detects and fills missed export windows
What's NOT Changing (Yet)
The actual export logic in adwyze (
DataExportJob,CsvTarget, S3/GCS uploads) stays the sameWidget exports are NOT part of this migration (they stay on Airflow)
Export configs are still managed in Retool via
DataExportConfigThe Mozek DAGs use the Sidekiq executor, so they ultimately enqueue the same export jobs
Related Work
This is part of a broader pattern (documented in ADR 0045) of migrating workloads from Airflow/cron to Mozek. The Adjust analytics channel is also being migrated for similar reasons (better failure handling, reduced redundant work).
Common Questions from Support Threads
"When will the customer see their widget export?"
Check the customer's WidgetExportConfig for export_hour and interval, then convert to UTC using the company's timezone. The pipeline runs every hour and picks up configs that are due. Typically the export completes within 1-2 hours of the scheduled time, depending on queue depth.
"When will the customer see their data export?"
Data export primary: After 2 PM UTC (or 9:30 AM UTC for DH)
Data export secondary: After midnight UTC (or 4:30 PM UTC for DH)
"Can we change the export time for a customer?"
Widget exports: Yes — update
export_hourin the WidgetExportConfig metadata via Retool. Remember this is in the company's timezone, not UTC.Data exports: Requires a tenant-level cron override or a custom cronjob entry. Coordinate with engineering.
"The export didn't run today"
Check in this order:
Widget exports:
Airflow UI — was the
widget-exporterDAG triggered?Check
CustomSchedulerlogic — is the config'sexport_hour+intervaldue?Check
last_job_started_aton the config
Data exports:
DataExportLogtable — check for STARTED/COMPLETED/FAILED statusSidekiq dashboard — is the job stuck in retry?
Mozek dashboard (once migration is complete)
Architecture Summary
Widget Export Pipeline (Current)
K8s CronJob (every hour, 0 * * * *) → Airflow queue (ingestion:queue rake task) → Airflow DAG Runner (widget-exporter DAG) → K8s Pod runs export:widget[company_id] rake task → CustomScheduler.time_to_run? per config → WidgetExporter.export (inline, NOT Sidekiq)
Widget Export Manual Run
Retool → schedule_export controller → WidgetExportJob.perform_async (Sidekiq) → widget_exporter queue → WidgetExporter.export with temp config
Data Export Pipeline (Current)
K8s CronJob (cron schedule per channel) → data_exporter:for_channel[channel] rake task → DataExportJob.run → Per-config export via CsvTarget → S3/GCS
Data Export Pipeline (After Mozek Migration)
Mozek Orchestrator → data_export / data_export_secondary DAG → Sidekiq executor → DataExportJob.run (same export logic) → Per-config export via CsvTarget → S3/GCS + Automatic retry (5-day window, 10-min intervals) + Non-overlapping guarantees + Gap detection & fixing
Key Code References
Widget scheduling logic:
lib/widget_exporter.rb→export_configs_to_runCustomScheduler:
lib/custom_scheduler.rbWidget Airflow DAG:
python/ingestion_pipeline/airflow/dags/dags/generated/widget_exporter/widget_exporter.pyWidget manual export:
app/sidekiq/widget_export_job.rb+app/controllers/widget_export_configs_controller.rbData export crons:
cron_job/jobs/production/data_exporter_jobs.rbMozek export DAGs (branch):
test/mozek/v3/fixtures/dags/data_export.jsonMozek tenant types:
lib/mozek/v3/orchestrator/tenant.exADR 0045 (scheduling migration rationale):
adrs/0045-analytics-scheduling.md