Foundations · Goals
Goals, users and numbers
Who uses omnium, what it must do, and the numbers every later part of the design is held to.
In one minute
Omnium is a product, not a one-off fix. It replaces a legacy .NET scoring system, one sport at a time. Football and curling go first. Cricket, the biggest earner, comes later, but the base is designed for cricket from day 1.
Data comes in from many sources, often on the same match: scorers in the console, data-provider APIs, Scout jobs and hand edits. Every source takes the same road (commands), and a higher-priority source wins over a lower one. Data goes out to B2B clients (about 90 in 100 of our users) through files, APIs and push, and to fans through public APIs on client websites.
Section 1 turned "never lose data" and "no downtime" into eleven targets we can test. The four that shape most of the design: no action lost from the tap on, API and push updates reach the client within 2 s (95 in 100), FTP and SFTP files within 10 s, and 10 million public API requests a day, with peaks of 2,500 a second.
Most of these targets are agreed design. Today's code meets only parts of them; the last table on this page shows which.
What this part does
Section 1 answers five questions before anything is designed. Every later section depends on these answers, so a guess here would be a guess everywhere.
| # | Part | The question | What it fixes later |
|---|---|---|---|
| 1 | Users | Who puts data in, who checks it, who watches it, who takes it out? | Roles and sign-in, the screens, the control screen |
| 2 | Events and sports | Which events come next? Which sports need event-by-event scoring, and which only results? | How much live scoring we build, which sports first |
| 3 | Size at peak | How many matches at once, actions per second, clients and fans? | The database, live updates, cache or push |
| 4 | Targets | How correct, how fast, how often up, how fast back after a failure? | Save and retry rules, backups, alerts |
| 5 | First release | Fix what runs today first, or build live scoring first? | The order of all the work |
The answers came from the product owner in the design doc's question tables (1.1 to 5.2, F1 to F11, G1 to G6). They are written up below in teaching order, not in the order they were asked.
The users

| User | What they do | What they need from omnium |
|---|---|---|
| Scorers (our operations team) | Score matches live in the console, from the office, a venue, or anywhere | A screen that shows a tap at once and never loses it, even on a bad network |
| Commentators | Add editorial commentary, often on a second console for the same match | Their own lane, so they never block the scorer (D9) |
| Console staff | Fix wrong data by hand, watch read-only screens and stats dashboards | Fixes that stick (an import must not undo them) and bulk edits with a preview, without a developer |
| Data providers | Send data through their APIs, pushed to us or pulled by us | To be registered once, by settings, and turned into the same commands as a scorer's |
| Scout | Our own scraper jobs that read official results sites | The same road in as every other source |
| B2B clients | Media, leagues and others: NDTV, News18, DailyHunt, Google and more | The right score, fast, in the exact format their contract says |
| Fans | Read scores on client websites and apps, some through our public API | Fast pages, at any crowd size |
| Person on duty | Watches the system on a live day | An alert when something breaks, not a log to read (D8) |
What we knew before Section 1, from the Asian Games (Aichi–Nagoya, 19 Sep to 4 Oct 2026), our only real load so far:
- No live scorers. Every change came from Scout imports or hand edits in the console. The ball-by-ball scoring screens were built but never used on prod.
- Three media clients. NDTV and News18 got 7 files each, by FTP, SFTP or S3. DailyHunt got the top-5 medal table only, from 30 Sep.
- No targets were written down. No delay target, no uptime target, no "how much data may we lose" target. Real delay from the official site to a client file was minutes: Scout read the site every 1 to 10 minutes, then the send rounds followed.
How it works
The targets sit along the path one score takes, from the scorer's tap to a fan's screen. Each step has one number it must meet, and each number has a reason.

- The scorer taps. The screen shows the action at once. The round trip from India to our servers in AWS US East is about 200 ms (from the design doc), so the screen must not wait for the server to show it.
- The device keeps it. The action stays on the scorer's device until the server confirms it. If the venue network drops, scoring goes on, and the actions sync when the network is back (targets 5 and 6).
- The server saves, then confirms. The server says "saved" only after the save is on disk. This is what makes "no data lost" true from the tap on (target 5). If the database fails over to its standby, clients see at most 2 minutes of extra delay, and scorers keep scoring on their devices (target 6).
- The sender tells the clients. For APIs and push, 95 in 100 updates reach the client within 2 s of being saved. For FTP and SFTP files, within 10 s. A client's contract may allow slower; nothing faster is promised (target 4).
- The cache answers fans. Public API reads go through a cache. The database sees a few reads per match per second, however many fans there are. The public API must be ready for 10 million requests a day and peaks of 2,500 a second (target 10).
Around the path there are four more rules. Every action records who did it and from which screen (target 8). Results go out at once by default, and a checker step can be switched on per sport or client later (target 7). A new client feed, provider or sport is added by settings, in 1, 3 and 5 days (target 11). And one person is on duty, with a minimum set of alerts, on every live day (D8).
A worked example
Example · A goal in the 63rd minute, on a busy day (agreed design)
This follows one football goal through the agreed targets. The times are the targets, not measurements. Nothing on this path is measured yet.
- 19:42:10.000. A scorer in the office in India taps "Goal, home, player 9". The console shows 1–0 at once and marks the action "unsent: 1".
- 19:42:10.000 to 19:42:10.200. The action travels to US East and back. That round trip is about 200 ms (design doc, point 7). The office wifi drops for 40 s just after the tap.
- During the 40 s. The device keeps the action. The scorer taps a yellow card too; it is kept in order behind the goal. Nothing is lost (target 5) and scoring did not stop (target 6).
- 19:42:50. The network is back. The device sends the goal, then the card, in order, each with the same id it had from the start. The server saves each one on disk, then confirms. "Unsent" goes to 0.
- Within 2 s of the save. A client on push or API, such as Google, has the goal in 95 updates out of 100 (target 4).
- Within 10 s of the save. The FTP file for a media client such as News18 holds the goal (target 4).
- The next minute. The goal brings 10 to 20 times the usual fan traffic to client websites that use our public API. That multiplier is an estimate from the design doc; we have no data yet. On a 10-million-request day, the average is about 116 a second, so 20 times is about 2,320 a second. The cache answers them. Postgres sees a few reads for this match each second (target 10).
- The log. The goal's row says who scored it (the scorer's account) and which screen it came from (target 8).
Example · A wrong value after an import (agreed design, part built today)
- A provider feed and Scout both cover a curling match. The provider has higher priority, so its score wins (D6).
- Scout's lower value is still saved, just not published. A difference between the two can raise an alert: a free second check.
- A console user spots a wrong end score and fixes it by hand. A hand edit sits above both feeds in priority, so the next import does not undo it.
- Today the "hand edit beats import" part is built, as a pin on the field (see Low-level design). Per-field priority between many sources, and keeping the lower value, are agreed in Section 2 and not built yet. See Truth and ownership.
Low-level design
The eleven targets
These are the targets agreed in Section 1. Every later section is checked against them. Each one says what it means and why it is set where it is.
| # | Target | Value (agreed) | Why this number |
|---|---|---|---|
| 1 | Sources of data | Scorers in the console, data-provider APIs and Scout, often mixed on one match. More sources will come. | The Asian Games mixed Scout and console on the same units; every source must meet in one place |
| 2 | Sports | Any sport. Top 5: cricket, football, kabaddi, hockey, F1. 15 to 20 scored live. First on the new system: football and curling. Cricket later. | Football and curling are slow sports, so they prove the base without cricket's pace or risk |
| 3 | Size | 10 matches a day now, ready for 50. Built for 10 times that (500 a day, 100 at once) at little extra cost. | 50 matches a day is a few actions a second at most (design doc estimate); Postgres handles that easily |
| 4 | Delay to clients | APIs and push: 95 in 100 updates within 2 s of being saved. FTP and SFTP files: within 10 s. Slower only where a client's contract says so. | Ahead of TV (about 19 s) and of most media data products (4 to 20 s) |
| 5 | Data loss | None, from the tap on. The device keeps every action until the server confirms it, and the server confirms only after the save is on disk. | "Never lose data" made testable |
| 6 | Downtime | Scoring never stops: scorers keep working through an outage, and actions sync after. Clients see at most 2 minutes of extra delay while the database fails over. Releases cause no downtime. | "No downtime" made testable; no system can promise nothing ever fails |
| 7 | Checking | Publish at once by default. A checker step can be switched on per sport or client later, without a redesign. | Most entries go straight to clients today; a checker must be possible later |
| 8 | Who did what | Every action records the person and the screen it came from. | Needed for priority, audit and fixing mistakes |
| 9 | First release | The base first (scoring worker, workflows, console), ready in one month and proven with football and curling. Live scoring is the end goal. | "Focus on foundation" (answer 5.1) |
| 10 | Public API load | Ready for 10 million requests a day (about 116 a second on average) and peaks of 2,500 a second. A cache answers fans, so their number does not change the load on the database. | "Millions of requests a day" (answer F8), with room for match spikes |
| 11 | Adding things | A new client feed in 1 day, a new provider in 3 days, a new sport in 5 days, without changing the core. Operations fix data in the console without a developer. | "Build it as a product": new things come by settings, not code |
What targets 5 and 6 cost
Decision D11 says "never lose data" and "no downtime" mean exactly targets 5 and 6, and we test them. The design doc names the price plainly:
- a queue on the scorer's device, so actions survive a network drop;
- a standby database in a second zone, which is where the 2-minute failover comes from;
- two copies of every service, and releases that swap one copy at a time.
The device queue also explains decision D10. "Any device" includes Safari on iPhone and iPad, and Safari can delete data a site keeps on the device after 7 days with no visit, and when a private window closes. So the console works in any modern browser, but three setups are tested and certified for live scoring: Chrome on a laptop, Safari on an iPad, and Chrome on an Android phone. The console warns when it cannot keep data on the device.
The load, worked out
The arithmetic behind targets 3 and 10:
seconds in a day 86,400
1 million requests a day -> about 12 a second (1,000,000 / 86,400)
10 million requests a day -> about 116 a second (10,000,000 / 86,400)
a goal spike, 10 to 20 times average (estimate, design doc; no data yet)
20 x 116 -> about 2,320 a second -> target peak 2,500 a second
The point of the design doc's view 8: the load is small; correctness and delivery are the hard parts. The writes are a few actions a second. The real load is on the way out: many clients, many formats, and public APIs on client websites. Every public read needs a cache in front of it.
Who did what: the log today
Every change in omnium today is a workflow command, and every command lands as one row in timeline_item before any writer runs. The row carries who sent it. This is the base target 8 builds on.
# packages/core/src/omnium_core/workflows/records.py:45-62
PERSON = "person"
INTEGRATION = "integration"
@dataclass(frozen=True, slots=True)
class Actor:
"""Who sent a command."""
#: ``person`` or ``integration``.
kind: str
#: What the log shows: an account's name, or ``integration:<code>``.
name: str
#: ``timeline_item.source_code``: the account id, or the integration code.
source_code: str | None = None
...
Actor is the sender of one command. kind says whether a person or an integration (a Scout import) sent it. name is what the log shows. source_code is the id that makes a retry safe: two senders can each use the same idempotency key without clashing.
# packages/core/src/omnium_core/workflows/engine.py:141-154
item = TimelineItem(
stream_id=stream_id,
fixture_id=fixture.id if fixture is not None else None,
source_code=actor.source_code,
event_type=code,
unit=UNIT,
seq=await streams.next_seq(session, kind, subject_id),
status=TimelineItemStatus.CONFIRMED,
occurred_at=datetime.now(UTC),
payload=_logged(node.impl, payload),
idempotency_key=idempotency_key,
actor=actor.name,
)
This is where the log row is made. actor and source_code record who sent it. seq is the next number in this match's (or event's) log. idempotency_key is the client's own id for the action, so a re-send is spotted and not applied twice. For how commands, validations and actions are written, see Writing a workflow in code.
Two gaps against target 8, both checked in the code:
# packages/admin/src/omnium_admin/auth.py:31, 52-56
NO_LOGIN = "admin"
...
@property
def actor(self) -> Actor:
"""A person, for the workflow engine. The account id is what makes a retry safe."""
source = f"account:{self.signed_in.account_id}" if self.signed_in is not None else None
return Actor(kind=PERSON, name=self.name, source_code=source)
- The person can be "admin". When nobody is signed in, the log says
adminandsource_codeis empty. Sign-in is off by default:admin_auth_required: bool = False(packages/core/src/omnium_core/settings.py:56). - The screen is not recorded.
timeline_itemhasactorandsource_code(packages/contract/src/omnium_contract/models/timeline.py:76-78) but no column for the app or screen the command came from.
Sources and priority: the pin today
Decision D6 (a higher source beats a lower one) is settled in Section 2. Today there is one built level of priority: a person beats an import. A field a person set in the console is "pinned", and imports leave it alone.
# packages/core/src/omnium_core/ingest/fixtures.py:1141-1150
pinned = set(row.pinned or [])
changed = False
def allowed(name: str, same: bool) -> bool:
if same:
return False
if name in pinned:
report.kept_by_hand += 1
return False
return True
For each field an import wants to change, allowed asks two things. Is the value the same? Then skip it. Did a person pin this field? Then keep the person's value and count it in kept_by_hand. The pin lists live on fixture.pinned and fixture_competitor.pinned (see the module note at packages/core/src/omnium_core/ingest/fixtures.py:33-38).
Provider feeds: not built today
Decision D5 says provider data takes the same road as scorer data: no side road, raw data kept. Today, the only no-code source is a Scout job. An integration row connects one Scout job to one workflow's import command (packages/contract/src/omnium_contract/models/integrations.py:27-43, with scout_job_id at line 43). The seam meant for live provider feeds does nothing yet:
# packages/worker/src/omnium_worker/ingest/runner.py:293-299
async def _fold_command_stub(fixture_id: uuid.UUID, record: RawRecord) -> None:
"""Tier-1 fold seam — no-op until the provider sample lands.
The real mapper turns ``record.payload`` into a scoring command,
``ledger.append``s it and enqueues the scoring job (which folds).
"""
return None
A provider message that reaches this seam is turned into nothing. Agreed design, not in the code yet: each provider is registered once by settings (address, login, push or pull, how often), its raw data is kept, and each message becomes the same commands a scorer sends.
Delay to clients: what the code does today
Target 4 is measured from the save. Today's path to a client file has three timers, all settings with these defaults:
# packages/core/src/omnium_core/settings.py:143, 154, 238
feed_debounce_seconds: float = 1.0 # a burst of writes -> one feed rebuild per window
feed_rebuild_seconds: float = 60.0 # sweep: is any event's data newer than its feeds?
delivery_poll_seconds: float = 5.0 # sender asks "who is owed a file?"
feed_debounce_secondsjoins a burst of writes into one feed rebuild per second.feed_rebuild_secondsis a safety sweep. Writes from another process (such as imports) may not reach the feed engine directly, so once a minute the delivery worker checks for feeds behind their data.delivery_poll_secondsis how often the sender reads the database to find clients who are owed a newer file (packages/core/src/omnium_core/publishing/delivery/dispatcher.py:1-27).
Read from these settings alone (an estimate, not a measurement), a console edit can reach an FTP client in about 1 s plus up to 5 s plus the upload, which fits the 10 s target on paper. A change that waits for the 60 s sweep does not. Neither path is measured end to end today: not checked. The 2 s target for APIs and push needs a different path, designed in Stats, feeds and delivery.
Public reads: the cache today
Target 10 needs a cache in front of every public read. Today the public API has a Redis cache with time limits by how fast the data changes:
# packages/core/src/omnium_core/settings.py:77, 92-103
redis_url: str | None = None
...
cache_ttl_live: int = 3
cache_ttl_scheduled: int = 60
cache_ttl_final: int = 21_600 # 6h
cache_ttl_reference: int = 3_600 # 1h
cache_ttl_leaderboard: int = 60
cache_ttl_negative: int = 45
cache_stale_window: int = 30
Live data is kept 3 s, finished results 6 hours. cache_stale_window lets a key be served for 30 s more while it is refreshed in the background. redis_url is optional, so an environment with no Redis has no cache at all. The 10-million-a-day and 2,500-a-second figures have not been load-tested on this branch: not checked.
Adding things by settings (target 11)
| Thing to add | Target | Today |
|---|---|---|
| A client feed | 1 day | Settings. A feed is a saved GraphQL query in feed_definition; ops change what it selects with no deploy (packages/contract/src/omnium_contract/models/feeds.py:1-10). Sending is a delivery_assignment row. |
| A provider | 3 days | Settings only for Scout jobs (integration). Any other provider needs code, and the seam for it is a stub. |
| A sport | 5 days | Code: a new package of six files in the flows plugin (see Writing a workflow in code). The core does not change. |
| A data fix | no developer | Console commands for single fixes. A bulk edit with a preview does not exist: on 1 Oct a script was needed to fix 296 shooting values. |
When things go wrong
What each failure must look like when the targets are met (agreed design):
| What goes wrong | What happens |
|---|---|
| Venue or office network drops mid-match | The scorer keeps scoring. The device keeps every action and sends them in order when the network is back. Nothing is lost (targets 5, 6). |
| The server gets an action twice (a retry) | The action carries the same id on every retry. The server spots it and does not apply it twice. |
| The server crashes just after a save | The save is on disk before the "saved" reply, so nothing confirmed is lost. Today this is not true: see the next row. |
| Today: the process dies after replying, before the commit | The console edit is lost and nobody is told. The commit runs in get_write_session after yield (packages/admin/src/omnium_admin/deps.py:24-32); the research note of 30 Sep found FastAPI runs it after the reply is sent. This breaks target 5 and is fixed in the rebuild. |
| The database fails over to its standby | Scorers keep working on their devices. Clients see at most 2 minutes of extra delay (target 6). |
| A release is deployed during a match | Releases swap one copy of a service at a time. No downtime (target 6). |
| The top-priority source goes quiet mid-match | An operator takes over with one switch, or a lower source takes over after a set time. Settled in Section 2. |
| A goal brings a traffic spike | The cache answers fans. The database sees a few reads per match per second (target 10). |
| Safari deletes data kept on the device | Unsent actions are normally sent within seconds, so the risk is small but not zero. The console warns when it cannot keep data on the device (D10). |
| Something breaks and nobody notices | Month 1 has a minimum alert set in one Slack channel: a live match with no update for too long (set per sport), a client file that fails to send, and a worker that is down. One named person is on duty per live day (D8). On 1 Oct the delivery worker failed every 5 to 7 minutes and was found by reading logs, not by an alert. |
Decisions
All fifteen were agreed on 3 Oct 2026. D12 to D15 came from answers G1 to G4.
| # | Decision | In plain words |
|---|---|---|
| D1 | The new system is a product. It replaces the legacy .NET system, sport by sport. | This is a migration as well as a build. |
| D2 | Football and curling go first. Cricket comes later, on the same foundation. | Slow sports prove the base; cricket is too important to risk first. |
| D3 | The foundation is designed for cricket from day 1. A recorded cricket match must run through it in a test before the foundation counts as done. | If cricket does not fit, we find out in week 3, not month 4. |
| D4 | Month 1 ends with football and curling running in shadow next to the legacy system. Clients switch one by one after two clean weeks. | Same real matches, both systems, outputs compared after every match. |
| D5 | Every source (scorer, provider, Scout, hand edit) goes through the same commands. Provider data gets no side road. Raw provider data is kept. | One place where all values meet, so priority, "never lose data" and "who did what" can work. |
| D6 | Sources win by priority. A higher source overrides a lower one, never the other way round. The details are settled in Section 2. | See Truth and ownership. |
| D7 | The whole thing is called the workflow platform. Scoring is one kind of workflow. | Sources in, workflows in the middle, feeds out. Commentary, results and Games operations are workflows too. |
| D8 | Month 1 includes a minimum alert set and one named person on duty per live day. Full alerting comes later. | Nobody can keep a promise they cannot see breaking. |
| D9 | Commentary is its own lane, linked to the match and, where it fits, to a ball or event. How clients get it is decided in Section 9. | A commentator must never make the scorer's actions out of date. |
| D10 | The console works in any modern browser. Three setups are tested and certified for live scoring. | Chrome on a laptop, Safari on an iPad, Chrome on an Android phone. |
| D11 | "Never lose data" and "no downtime" mean targets 5 and 6, and we test them. | Goals become tests we can run. |
| D12 | Client feeds moved from the legacy system keep exactly the same responses: format, names and ids. | A client must not notice the switch. |
| D13 | Nothing is designed for one sport. Every part works for any sport, at any depth of detail. | Football goals-only or every event; curling per end or every stone. |
| D14 | A source can be anything: a scorer, a provider that pushes or is pulled, a file, Scout, or a hand edit. | New source kinds are added, not designed around. |
| D15 | The shadow run starts only when the base is fully built and tested. | No fixed start date; the base being ready is the gate. |

Built today, or still to build
Checked against the si-build-omnium code on 7 Oct 2026 (branch feat/result-feeds, commit 76ef1e2).
| Piece | Today | Agreed design | Status |
|---|---|---|---|
| One road for every change | Every write is a workflow command logged in timeline_item (packages/core/src/omnium_core/workflows/engine.py:141-156) | Same, for every source kind (D5, D14) | Partly built |
| Who did what | Person name and account id, or integration:<code> (workflows/records.py:50-58). "admin" when nobody signs in (admin/auth.py:31). No screen recorded. | Person and screen on every action (target 8) | Partly built |
| No data lost | Server can reply before it commits (admin/deps.py:24-32; research note 30 Sep). No device queue in the console (research note 02-console-and-bridge.md; not re-checked). | Device keeps actions; server confirms after save (target 5) | Agreed, to build |
| No downtime | Not checked in this repo (infrastructure). | Standby database, two copies of every service, rolling releases (target 6) | Agreed, to build |
| Delay to clients | Debounce 1 s, sender poll 5 s, sweep 60 s (core/settings.py:143, 154, 238). Not measured end to end. | 2 s for API and push, 10 s for files, 95 in 100 (target 4) | Partly built |
| Public API cache | Redis cache, live data 3 s (core/settings.py:92); optional (redis_url, line 77). Not load-tested here. | 10 M a day, 2,500 a second peak, fans never reach the database (target 10) | Partly built |
| Priority between sources | Person beats import, by pins (ingest/fixtures.py:1141-1150) | Per-field priority across all sources, lower value kept (D6) | Partly built |
| Provider feeds | Scout jobs only (integration.scout_job_id). Provider seam is a no-op (worker/ingest/runner.py:293-299). | Any provider, registered by settings, raw data kept (D5) | Agreed, to build |
| Checker step | None in the workflow engine (searched for checker or approve: no match). | Off by default, switchable per sport or client (target 7) | Agreed, to build |
| Minimum alerts | A Slack sender exists (core/notify/slack.py) but no service calls it. | Three alerts in one channel, one person on duty (D8) | Agreed, to build |
| Football | Plugin at packages/flows/src/omnium_flows/football/, no substitutions (no match for "substitut"). | Any depth of detail (D13) | Partly built |
| Curling | No plugin and no catalogue entry (no match for "curling" in packages/). | First sport with football (D2) | Agreed, to build |
| New client feed by settings | Saved GraphQL in feed_definition; delivery_assignment rows | 1 day (target 11) | Built today |
| Bulk edit with preview | Not built. | Ops fix data without a developer (target 11) | Agreed, to build |
| Certified devices | Not built. | Three setups certified, warning when the device cannot keep data (D10) | Agreed, to build |
| Shadow run against legacy | Not started. | After the base is built and tested (D4, D15) | Agreed, to build |
Numbers
| Number | What it is | Source |
|---|---|---|
| 44 sports, 59 disciplines, 470 events | Asian Games 2026 size | Design doc, "What we know today" |
| 5,531 units, 806 medal units, over 16 days | Asian Games 2026 size | Design doc |
| 45 countries, 10,825 people | Asian Games 2026 size | Design doc |
| 621 units | Busiest day (2 Oct) | Design doc |
| 160 units across 13 venues | Busiest half hour (from 10:00, 24 Sep) | Design doc |
| 662 units | India alone | Design doc |
| 3.4 MB | The calendar file | Design doc |
| 1 to 10 minutes | How often Scout read the official site | Design doc |
| about 200 ms | Network round trip, India to AWS US East | Design doc, point 7 |
| 10 to 20 times | Fan traffic after a goal, against the average | Estimate, design doc; no data yet |
| 116 a second | Average of 10 million requests a day | Arithmetic, 10,000,000 / 86,400 |
| 296 values | Shooting values fixed by script on 1 Oct | Design doc |
| every 5 to 7 minutes | Delivery worker failures on 1 Oct | Design doc |
| 0.35 s; about 4 s; about 19 s | Paris 2024 results to media; Wimbledon click to screens; broadcast TV behind the field | Research note 03-industry.md, published sources |
Read next
- The big picture: every part of omnium on one page.
- Life of a score: one goal from tap to client file, step by step.
- Truth and ownership: Section 2, where the priority rule (D6) is settled.
- Commands and the workflow engine: the one road every change takes, and the command-time numbers.
- Stats, feeds and delivery: how the 2 s and 10 s targets are met.