Scheduled jobs
What runs on a timer, what each job writes, and how a job that stops running becomes visible.
Two jobs run on a timer, and one check watches them.
What is scheduled
| Job | Schedule | Writes |
|---|---|---|
| MTGJSON price ingest | 09:00 UTC, daily | printing_prices_current, printing_price_daily, and the card_prices headline |
| Freshness check | 10:00 UTC, daily | Nothing. It reads and asserts. |
| Card chain | 11:00 UTC, daily | Everything else, when there is anything to do |
MTGJSON publishes its daily price file around 08:00 UTC. The ingest runs an hour later so a late publication does not turn into a missed day.
The card chain asks before it acts
The chain imports sets, precons, printings, card text, types, embeddings and tags, in the order each depends on the one before it. That means downloading well over two gigabytes, so running it every day unconditionally would spend that most days for nothing.
It starts with one request to Scryfall's set list and compares the card total against a watermark recorded when the chain last completed. Unchanged, and it records a skip and stops. Changed, and it runs the whole sequence and moves the watermark afterwards, so a chain that dies halfway is retried the next day rather than marked done.
Daily rather than weekly, despite the name, because a set's cards appear in previews over the weeks before it releases. A weekly timer would leave them up to seven days late for no saving, since a day with nothing new costs one HTTP call.
Prices are the one thing that has to be on a timer. Every other value can be rebuilt from a vendor's current file whenever somebody runs the import, but a daily price point exists only if something captured it that day. MTGJSON's history window is about 90 days, so a day nobody captured is gone once it ages out. That is not hypothetical: 29 days went missing before anyone noticed, and recovering them was a race against that window.
Nothing asks a vendor anything at request time. Every number the app shows was written by one of these jobs beforehand.
How a stopped job becomes visible
The check at 10:00 is deliberately a separate schedule rather than a step at the end of the ingest. A check that runs at the end of a job cannot detect that job failing to start, and failing to start is the failure that actually happened here: two earlier scheduled jobs were configured, deployed, and never fired once, because the platform invoked them with a request method neither route handled. There was no failed run to look at, because there was no run.
So the check reads the data rather than the schedule. It asserts that
printing_prices_current is at most two days old, that the daily series is at
most two days old, that the rolled-up headline was rebuilt within two days, and a
dozen other things besides. A job that stops running fails the check the next
morning.
What to read for freshness
Every imported value carries when it was written, and those timestamps are the answer to "is this current", not the schedule:
| Column | Answers |
|---|---|
printing_prices_current.price_date | The day that quote is from |
card_prices.updated_at | When the rolled-up cheapest rebuilt |
card_popularity.updated_at | When play ranks last moved |
sealed_products.prices_updated_at | The last sealed refresh, which has no vendor. See prices |
What is still manual
The chain deliberately leaves out the jobs that cost money or judgement: a full card resweep for errata, the model tagging pass, and the set guides. See imports and tagging and embeddings for what those jobs are and what each one writes.