Inbox ranking Dagster dags
Dagster jobs for the Self-driving Inbox report-ranking model.
The one dag today is the dataset dag; future ranking dags (training, eval) live as sibling subpackages sharing common.py.
inbox_ranking/
├── common.py # shared: S3 destination config, daily partition def, gating, Parquet IO
├── dataset/
│ ├── dag.py # the four dataset assets + job + schedule
│ └── queries.py # InsightsQL label SQL, embeddings SQL, stream merging
└── tests/
Registered via insights/dags/locations/signals.py (US only) and loaded locally through .dagster_home/workspace.yaml.
The dataset dag
inbox_ranking_dataset_job runs daily at 02:30 UTC (schedule default: running on prod US, stopped everywhere else — including hosted DEV and E2E, which have no dogfood project to read labels from) and builds four assets on one daily partition, each a Parquet object in S3:
s3://<bucket>/<prefix>/
├── inbox_report_state/v1/dt=YYYY-MM-DD/ # Postgres spine + report state + tabular features
├── inbox_report_embeddings/v1/dt=YYYY-MM-DD/ # report_id -> small-1536 vector as of snapshot end
├── inbox_report_labels/v1/dt=YYYY-MM-DD/ # cumulative label columns from dogfood telemetry
└── inbox_report_model_data/v1/
├── dt=YYYY-MM-DD/ # materialized join of the three (the training table)
└── latest/ # rewritten by the newest partition; warehouse tables point here
Partition semantics
- Partition
dt=Dsnapshots the eligible report inventory (promoted beforeD+1 00:00 UTC, or referenced by any label event before it). Every label aggregate is boundedevent_time < D+1 00:00 UTC. - Label columns are cumulative: later partitions strictly dominate earlier ones, so training reads features from
dt=Dand labels from any later partition, choosing the label-maturity window at read time. Late labels are never backfilled into old partitions. latest/advances monotonically: each write stamps asnapshot-datein S3 object metadata, and a partition rewriteslatest/only when it is at or ahead of whatlatest/holds. Delayed retries of the newest day repair it; backfills of older days never clobber it.- Rows for reports outside this dag's region are label-only: no Postgres state, no embedding, and
report_team_idnull (a US team id and an EU team id of the same number are different teams, so the label stream's id can't be merged in).status_event_team_idcarries the team the transition itself reported, which is the tenant attribution those rows do have. - Backfills are not fully point-in-time: labels are exact for any past day, embeddings are exact within the source table's 3-month TTL, but report state is read from Postgres as of the run (
features_observed_atflags this per row). - Report-state mutability reaches inclusion, not just feature values:
promoted_atis cleared on suppression and snooze, so a report promoted before the cutoff and suppressed after it leaves the spine unless a label event referenced it before the cutoff. Forward runs see this only for the 2.5 hours between the cutoff and the schedule; backfills see the full accumulated effect. Deriving the spine from immutable promotion history (signal_report_status_changedcarriespromoted_at) is the v2 fix.
Configuration
| Setting | Default | Meaning |
|---|---|---|
INBOX_RANKING_DATASET_S3_BUCKET |
unset | Destination bucket. Unset on Cloud makes every asset log and skip, so the dag can deploy before the bucket exists. Unset elsewhere falls back to the deployment's object-storage service (SeaweedFS in dev and CI). |
INBOX_RANKING_DATASET_S3_PREFIX |
inbox_ranking |
Key prefix under the bucket. |
Writes use boto3: ambient AWS config (the node role) when the dedicated bucket is set, the OBJECT_STORAGE_* endpoint and credentials otherwise. Readers (project-level warehouse tables, model training) use a separate read-only credential provisioned with the bucket.
Datastore posture
All reads route to the offline cluster replicas on Cloud (etl_workload()), carry the dagster run in log_comment, and the cross-team embeddings scan runs under explicit time/memory/spill guards (see queries.py for why that scan has no team_id sort-key prefix and why that is acceptable).
Operating it
- Backfill any day range from the Dagster UI; partitions start 2026-04-01 (the label epoch). Every asset sits in the
inbox_ranking_etlpool so concurrent partitions don't each start their own fleet-wide embeddings scan — the pool's limit is a Dagster deployment setting, provisioned with the bucket. - Failures alert
#alerts-self-driving(ownerteam-self-driving); assets retry twice with a 60s delay before failing a run. A UI-launched materialization runs under Dagster's implicit__ASSET_JOB, which carries no owner tag, so alert routing falls back to matching theinbox_report_asset-name prefix. - The job is capped at 3h via
dagster/max_runtime— the six label streams run sequentially, each allowed up to 600s, and the join and S3 writes come after them.
Deletion and retention
Partitions are immutable history, so a report deleted later keeps its rows (and vector) in partitions written before the deletion.
The embedding tombstone nulls the vector in every partition built after it, and status='deleted' flows through state from then on.
A re-run does not scrub a deleted report. Label telemetry lives in the dogfood project and outlives the Postgres row, so a re-run regenerates the report as a label-only row: the id, its action/status/dismissal labels, and its team id come back, only the state columns go null.
Scrubbing therefore means deleting the affected dt= objects (and rebuilding latest/), not re-running the partitions.
Nothing in the team-deletion path knows about this prefix today, so the purge is manual until that is wired up - tracked with the bucket provisioning, and nothing is at risk before then because the assets write nothing until the bucket exists.
The bucket is internal-only and access-restricted; a lifecycle policy for old partitions is a provisioning-time decision.