Joel Stevick

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
From the records to a card and back to the records

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 waitingOn the listBehind
An inquiry, an agreement to send, an email, an outreach replyat onceafter 2 working days
A client's or a visitor's chat messageat onceafter 24 hours
An invoice sent and unpaidthe day after its due dateat once
An agreement sent and not signedafter 7 daysat once
A payout owed to a developerafter 14 daysat once
A project's due date2 days beforeonce it has passed
A project under way with nothing recordedafter 7 daysnever: it is a question
A care plan's monthafter 30 daysafter 35 days
Prospects Find prospects found and nobody decided onafter 3 daysnever: it is upkeep
The Upwork syncafter 24 hoursafter 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
An email from a client, from my inbox to a card and off it

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

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.