SNCF Blog

I moved the kube-scheduler into a spreadsheet formula, and it passed the unit tests

September 8, 2026 · by Timur Tukaev

Here’s the thing that had been bugging me about Sheeternetes. The whole pitch is “the spreadsheet is the cluster” — Deployments, Nodes, Pods live in tabs, and the sheet is the source of truth. Except it wasn’t the whole truth. The scheduler — the part that actually decides which pod lands on which node — was a Python function reading the sheet from the outside. The sheet stored the state. Python did the thinking.

That’s a cheat, and it nagged at me. So I set out to remove the last bit of external brain: rewrite the scheduler as a spreadsheet formula. No Python, no Apps Script, no bash. One =LET(…) per pod that reads the Nodes tab and decides placement, using nothing but the functions built into Google Sheets.

It works. It reproduces bin-packing, capacity, spread, sticky placement, cordon, affinity, and taints — and it produces the exact same placement as the real Python scheduler on all nine of its unit tests.

The kube-scheduler is now a spreadsheet formula

The rule of the game

Sheeternetes’ scheduler is a pure function, schedule(deployments, nodes, existing). It walks deployments in order, expands replicas, and for each pod picks a node:

It’s greedy and sequential: each placement consumes capacity, which changes the decision for the next pod. That sequential-accumulation part is exactly what makes “do it in formulas” interesting, because formulas are supposed to be declarative, not step-by-step.

The challenge, then: reproduce a greedy, stateful, order-dependent bin-packer using only cells.

The layout

Two tabs. Nodes is the cluster’s capacity — one row per node:

name cpu_total mem_total fresh schedulable labels taints
node-a 4000 8192 TRUE TRUE disk=ssd
node-b 2000 4096 TRUE TRUE disk=hdd
node-gpu 8000 16384 TRUE TRUE disk=ssd gpu=true:NoSchedule

Pods is one row per replica, in the order the scheduler would process them. The last column, chosen, is empty — that’s what the formula fills in.

The trick to greedy accumulation is a growing range. When the formula in row r needs to know “how much CPU is already committed on node X,” it sums the rows above it:

SUMIFS($C$2:INDEX($C:$C, ROW()-1), $G$2:INDEX($G:$G, ROW()-1), node)

$C$2:INDEX($C:$C, ROW()-1) is the classic expanding range — rows 2 through “one above me.” Because row r only ever looks at rows < r, there’s no circular reference; the sheet just evaluates top to bottom, each pod seeing the ones already placed. That single idea turns a spreadsheet into a sequential bin-packer.

The formula

For each pod, score every node scalarly, then pick the best:

=LET(
  nm, FILTER(Nodes!$A$2:$A, Nodes!$A$2:$A<>""),   /* names, capacities, flags … */
  creq, INDEX($C:$C, ROW()), mreq, INDEX($D:$D, ROW()),
  sel, INDEX($E:$E, ROW()),  tol, INDEX($F:$F, ROW()), prev, INDEX($H:$H, ROW()),
  score, MAP(nm, LAMBDA(q,
     IF( AND( fresh(q), schedulable(q), not_excluded(q),
              affinity_ok(q), tolerates(q),
              free_cpu(q) >= creq, free_mem(q) >= mreq ),
         free_cpu(q), -1 ))),
  best, IF(MAX(score) < 0, "", XLOOKUP(MAX(score), score, nm)),
  IF(sticky_ok(prev), prev, best)
)

MAP(nm, LAMBDA(q, …)) scores each node; ineligible nodes get -1; XLOOKUP(MAX(score), score, nm) returns the winner — and because XLOOKUP returns the first match, ties break toward the first node in order, exactly like Python’s max(). Then the sticky check overrides with the previous node when it’s still valid.

Here it is running on a real cluster — one gold =LET(…) in the formula bar, and the chosen column filled by the sheet itself:

The Pods tab: the ‘chosen’ column is computed entirely by the formula

Read that placement — it’s not random, it’s the scheduler thinking:

That’s affinity, taints, bin-packing, spread, and capacity — all decided by a formula.

Does it actually match the Python scheduler?

This is the part I actually cared about. The Python schedule() ships with a unit-test suite — spread, capacity bin-packing, memory as a constraint, cordon-keeps-existing, affinity pins, taint repels, toleration allows, too-big-is-unschedulable. So I built a harness that runs each test case through both schedulers and diffs the placement.

Same scheduler, two substrates — identical output

Nine out of nine. Same pod on the same node, every case. Plus the richer multi-deployment demo above, which I also checked against Python: identical, down to which batch replica gets orphaned. The formula isn’t an approximation of the scheduler — on these cases it is the scheduler.

Three quirks that fought back

Getting there meant losing an afternoon to Google Sheets’ array semantics, which collapse in ways that don’t announce themselves. If you ever do this, here’s what will bite you:

Three Google Sheets quirks that fought back
  1. A constant lambda collapses the array. MAP(arr, LAMBDA(x, 0)) returns a single 0, not a vector — if the lambda ignores its variable, Sheets quietly gives you a scalar. Make the lambda use it: MAP(arr, LAMBDA(v, v*0)).
  2. Array-minus-array collapses too. Subtracting two FILTER/MAP vectors (ct - zc), both two elements long, gave me one value. The fix that made everything robust: never do array-to-array arithmetic — compute each element scalarly inside MAP(keys, LAMBDA(q, XLOOKUP(q, …))).
  3. API writes don’t adjust relative references. I wrote the same formula string into every row via the Sheets API — no fill-down — so $C2 in row 3 still pointed at row 2. Half a scenario placed pods based on the wrong pod’s data. Use INDEX($C:$C, ROW()) for “this row” and the expanding $C$2:INDEX($C:$C, ROW()-1) for “rows above.”

None of these throw an error. They just give you a wrong answer with total confidence, which is the worst kind. A benchmark that only confirms your priors isn’t a benchmark, and a formula that silently returns a scalar isn’t a bug report — you find it by diffing against the thing you trust.

Why bother

Because it closes the gap in the joke. “The spreadsheet is the cluster” was always a little dishonest while the scheduler ran in Python. Now the sheet doesn’t just hold the desired state — it computes the placement. Change a replicas count and the neighbouring cells re-derive which node each pod belongs on, live, with no process running anywhere. That’s about as sheet-native as scheduling gets.

And underneath the absurdity there’s a genuinely useful lesson about greedy algorithms in spreadsheets: the expanding-range trick (A$2:INDEX(A:A, ROW()-1)) is how you do any sequential accumulation — running placement, running balance, running anything — in cells, without a script. I did not expect a container scheduler to teach me that, but here we are.

Do not schedule production pods with a spreadsheet formula. Do read the formula — it’s a surprisingly clear way to see what a scheduler actually decides.

Try the live sheet, poke the numbers, watch the placement change: the demo is public and read-only.

Join in

New here? Sheeternetes is a container orchestrator whose entire control plane lives inside a spreadsheet — Deployments, Nodes, Pods and Events are tabs, a scheduler reads and writes cells, and real Docker containers run on the other end. It’s part of the Sheet-Native Computing Foundation (SNCF), a working parody of the CNCF: 30+ projects, a container standard, certification, live demos.

It reconciles.

Also on dev.to and Habr (RU). Join the community — Slack, Telegram, LinkedIn. ← Back to the blog