Story 4 · Running the shop
Teaching my site to tell me what I'm behind on
The first tab on my admin page is now called My tasks. It reads every record my site keeps, says in words what is waiting for me, which of it is late, and walks me through it one card at a time until I'm caught up. It does not send, charge or decide anything. It only tells me what to do next, and where.
Thirty features and one person
I counted, and my site has about thirty features for finding work, doing it and talking to the people it is for. There are the offers on the home page and the free demo. There is a chat with each client, a Guide that leads each project step by step, Ask Claude for questions across all of them, invoices and care plans, outreach from my own Gmail, Find prospects, the Upwork pipeline and a job-search assistant that answers recruiters for me. Four of those are assistants doing real work on their own: the Guide, Ask Claude, Find prospects and the job assistant.
And still I could not say, on a given morning, whether I was behind, or in what. Every feature knew its own part. An engagement knew its due date, an invoice its due date, a chat thread who wrote last, a proposal that it was waiting for my Confirm. Nothing put them side by side. The job assistant answered job mail, and nothing watched the rest of my inbox. The Upwork tab knew only when I had last synced it. So I wanted one place that reads it all against a plain idea of "behind", and then steps me through it.
Compute it, don't store it
The obvious build is a to-do list: a table of tasks, written when something happens and crossed off when it is done. I did not want that. A list like that is a second copy of the records, and it goes stale the day it is written: a client replies in the chat, and the task that says "answer them" was written before the reply, or was never written because the chat was not wired to it. A list kept as a second copy and refreshed now and then is what a database calls a materialized view. Mine is the other kind of view: worked out from the records themselves every time the tab opens.
So nothing stores a task. Opening the tab runs a set of rules, each one a short database query and a few lines of code: an inquiry still marked inquiry, an invoice sent and past its due date, a chat whose last message is the client's. Each rule turns what it finds into a card with a title, one sentence of why, the one thing to do and the place to do it. The same records give the same queue, however often it is built, and the same person sees the same list each time: the rules are idempotent, so there is nothing to keep in step.
flowchart LR
R[("The records: engagements, invoices, chat, proposals, the inbox, outreach, the Upwork snapshot")] --> U["The rules, in code"]
U --> M["Minus the marks"]
M --> C["One card at a time"]
C --> P["The place: Work, Chat, Gmail, Upwork"]
P --> X["The record changes"]
X --> G["The card is gone"]
X -.-> R
The one thing the tab writes is a mark. A card has Done, Later (tomorrow, in three days, next week) and Skip, which asks for one line of why. A mark is shared, so when a developer puts something off, I see that they did and until when. A mark holds only while the fact it was put on is unchanged. If I mark a chat done and the client writes again, the new message is newer than my mark, and the card is back. I call that re-arming: a mark can quiet a fact, never hide a newer one.
A card's Open button goes straight to the place where the work is done: the engagement's Guide, the chat thread, the Gmail thread, the Upwork tab. When I come back, the tab reads the records again, and the card I dealt with is simply not there any more. I never cross anything off by hand unless there is nothing to change.
What counts as behind
Each card is behind, due or upkeep. Each card belongs to a group, money, clients, replies, delivery or Upwork, and the list runs behind first, then due, then upkeep, money first within each. Behind means past the line the rule draws, due means waiting but inside it, and upkeep is routine that nobody is waiting on, like syncing Upwork or deciding on prospects. The tab's label counts only what is behind or due, so routine never makes the number look worse than the day is. The lines are figures in one place in the code, and this table is checked against them:
| What is waiting | On the list | Behind |
|---|---|---|
| An inquiry, an agreement to send, an email, an outreach reply | at once | after 2 working days |
| A client's or a visitor's chat message | at once | after 24 hours |
| An invoice sent and unpaid | the day after its due date | at once |
| An agreement sent and not signed | after 7 days | at once |
| A payout owed to a developer | after 14 days | at once |
| A project's due date | 2 days before | once it has passed |
| A project under way with nothing recorded | after 7 days | never: it is a question |
| A care plan's month | after 30 days | after 35 days |
| Prospects Find prospects found and nobody decided on | after 3 days | never: it is upkeep |
| The Upwork sync | after 24 hours | after 3 days |
Most of these say themselves. The one I thought about longest is the first. My home page promises a reply to every inquiry within two working days, so the rule counts working days, Monday to Friday, in New York, where I am and where the promise is kept. An inquiry that arrives at four on a Friday afternoon is not behind on Sunday, or on Monday morning; it is behind at four on Tuesday. Counting calendar days would have made every weekend look like a failure and taught me to ignore the number.
The one source I had to add: my inbox
Almost everything the tab reads was already in my records. The exception was email. Clients, prospects and colleagues write to my Gmail, and my site knew nothing of it. But something was already reading every new message: the job assistant looks at the sender, the subject and Gmail's short snippet of each one, every five minutes, and sorts it, to find the job mail. That sorting call is a classifier: one call to a small model, one word back. It used to answer recruiter, scheduling, application, alert or other. Now it has one more word, needs_reply, for a person writing about anything but a job and waiting for my answer.
For a message with that word, my site keeps a row with who sent it, the subject, when, and which thread it is in. It keeps none of the text: the card tells me who and what about, and I write the reply in Gmail. It never keeps a row for an automated sender, for my own mail, or for someone on my outreach list, whose reply outreach already tracks. On each later tick it reads the thread: a message from me after theirs means I replied, and the message gone from my inbox means I archived it, which is me deciding it needs nothing. Either way the card is gone. A row nobody dealt with is let go after 60 days, and a handled row is deleted 30 days after.
sequenceDiagram participant K as A client participant G as Gmail participant T as The five-minute tick participant C as Claude Haiku participant S as My site participant J as Me K->>G: An email T->>G: Anything new? G-->>T: Its sender, subject and snippet T->>C: Sort this one C-->>T: needs_reply T->>S: A waiting row: sender, subject, time S-->>J: A card: reply to them J->>G: My reply, written in Gmail T->>G: Anything from me in that thread? G-->>T: Yes T->>S: Handled S-->>J: The card is gone
A developer's version
A developer who delivers projects for me gets the same tab, with only what concerns the projects assigned to them: their inquiries and due dates, their clients' chat threads, their Guide's proposals, their care plans' monthly runs. They never see money, my inbox, my job search, outreach or Upwork. That is scope by assignment: what a person sees follows what is assigned to them, and every rule checks the role and the assignment on the server, whatever the page asks for. A developer cannot even mark a task that is not in their own queue; the site works their queue out again and answers as if the task did not exist.
What it is not allowed to do
- It only reads. Nothing on the tab sends, charges or changes a record, except its marks. Open takes me to the place where I do the work myself, and anything there that reaches a client still waits for my Confirm. That is human in the loop: every action is a person's, and the tab only says which one is next.
- No model decides what is due. The rules are code, so I can read them and test them, and they cannot be talked into anything. The only model involved is the small one that already sorted my inbox.
- A mark never hides a fact that has changed since. Done, Later and Skip hold only while the record they were put on stays as it was.
- One broken source never empties the list. Each rule runs on its own. If one source cannot be read, the tab lists everything else and says which source it could not read.
The foot of the tab says when each source last looked: when my inbox was last scanned, when Upwork was last synced, when outreach replies were last checked, and "never" or "not connected" where that is the truth. That line is the tab's observability: it tells me what the queue can know and what it cannot, so an empty list means caught up and not asleep.
What it costs
Nothing new. The sorting call on each new message was already being made and paid for; it answers one more word now. The queue itself is a handful of database queries each time the tab opens, on records the site already keeps. Ask Claude reads the same queue when I ask it what needs doing, so its answer starts from the same list as the tab.
This story is about the build, not the results: it went live the day it was built. When there are weeks of it worth reporting, their numbers will come from an export of the records, as every number in these stories does.
What this means for your project
The shape is not about my business. An operations queue over the records a business already has, with its thresholds in code, checked on the server for each person, and a person at the keyboard deciding every step, is a small custom build, with tickets, orders, claims or contracts in place of my engagements and invoices: AI built on your company's data. The useful part is not a clever model. It is reading the records you already trust, in one place, and saying plainly what is late.