Skip to content
All posts

Filetable· Part 1 of 3

Queues and priorities: orchestrating thousands of documents with Temporal

A document goes through a dozen steps of OCR, AI and SQL. The real challenge is not chaining them, it is deciding who goes first.

7 min readLire en français

When you drop an invoice into Filetable, it goes through a series of steps: reading the PDF, picking the right table, extracting every field, linking suppliers or orders, filing it into a folder. Some take a tenth of a second, others two minutes. And sometimes hundreds arrive at once.

Why Temporal

A process like this has three enemies: external services that go down, servers that restart, and time itself. You can build it all by hand, with a message queue, statuses in the database and scheduled jobs to catch whatever got lost. You then end up rewriting, less well, what Temporal already does.

Temporal is an open-source durable execution platform: it guarantees that your code runs to completion, whatever fails along the way. Built by the creators of Cadence, Uber's workflow engine, it now runs, according to its website, products at OpenAI, Replit and Cursor. You write the workflow as ordinary code, step by step, and Temporal records the result of each one. If a worker stops halfway, another one resumes exactly where it was: nothing is redone, nothing is lost. Retries, timeouts and waits of several days become lines of code, not infrastructure.

Temporal

Durable execution

Worker A stops in the middle of extraction. Temporal kept the result of the first two steps: worker B resumes at extraction, redoing nothing.

Temporal is available as a managed service, Temporal Cloud, billed by usage. But the server is open source, under the MIT license, so Filetable runs it itself, in its own cluster, next to the application and the database. Workflow data stays in-house, no step pays for a round trip to another region, and the bill does not depend on how many steps run. The trade-off: operating, upgrading and monitoring it is on us. I say more about this in Launch fast, then last.

So every document has its workflow, and every step is an activity: a piece of work Temporal hands to a worker, retries when it fails, and whose result it keeps.

TemporalOne workflow per document
OCROCR readAImodel callSQLSQL write
  1. Read the documentOCR
  2. Pick the tableAI
  3. Extract the fieldsAI
  4. The row appearsSQL
  5. Relations × NAISQL
  6. FinalizeSQL
  7. File into a folderAI
  8. “Done”SQL
A document's journey. Each step is an activity, sent to the queue that matches its work.

The challenge. These steps have nothing in common. A call to the AI model can take two minutes; writing “Done” on one row takes a fraction of a second. When fifty documents arrive together, the tiny write waits behind the big calls, and the user watches a finished document keep spinning.

Priority is not enough

Temporal can prioritise: within a queue, an urgent task jumps ahead of the others. But priority only orders the backlog. It never frees a worker already busy with a two-minute call: if every slot is taken, the urgent write still waits.

The answer lies elsewhere: one queue per kind of work, each with slots of its own. Short writes never queue behind AI again, and each queue is sized for what it really costs.

Temporal

One queue per kind of work

backlog → slots
fast laneshort writes someone waits on
P3cleanupP2new rowP1“Done”
relations laneone cell at a time
P3cells × NP1on screen
AI lanethe heavy work: reading, model
P5housekeepingP3extractionP3readingP2filing
OCROCR readAImodel callSQLSQL write
Each queue has slots of its own. Within a queue, priority is strict: P1 goes before P2. But it only orders the backlog; it never frees a busy slot.

Measured on real batches, before and after:

Before → after separate queues

> 2 min
0.1 s

wait to write “Done”, a single row

45 s
16 s

median wait before extraction

Measured on real batches of a few dozen documents. Short writes were under 1% of the work, yet waited behind two-minute model calls.

Fair between customers

Within one queue, a customer uploading 500 invoices must not hold up one uploading a single one. Temporal can share tasks by fairness key: every workflow carries its workspace's, weighted by its plan. A customer alone gets the whole capacity; with several, each moves at its own pace.

Dispatch order

customer Acustomer B
Same weightABABABABABAB
Weight 1 vs 3BBBABBBABBBA
B aloneBBBBBBBBBBBB
Within one priority, tasks are shared between customers by weight, here their plan. A customer alone gets the whole capacity, and nobody is ever skipped.

Waiting without blocking

When the AI provider is saturated, it refuses calls, sometimes for twenty minutes. It is not the document's fault: you wait, then try again. But where you wait changes everything. An activity that sleeps keeps its slot for nothing; a workflow timer costs nothing, and even survives a restart.

AI provider overloaded

tryslot held for nothingslot free
An activity that sleeps1′2′3′4′5′
A workflow timerTemporal1′2′3′4′5′
The provider refuses calls for many minutes. The workflow waits 1, 2, 3, 4 then 5 minutes between tries: a timer holds no slot, while an activity sleeping through it would keep its own the whole time.

What I take from it

  • Durable execution changes how you write a back end. Retries and waits become readable code.
  • Priority orders, it does not free. Split queues by how long the work takes.
  • Measure before tuning. A step that weighs under 1% of the time can still keep the user waiting two minutes.
  • A wait lives in the workflow, never in an activity that keeps its place.

Maxime Burri

Working on something similar? Let's talk.

Send an email