// askcal • a daily scheduler

Your inbox, ranked by regret.

Not everything in your inbox deserves your attention. Askcal works out what will actually cost you something if you ignore it — then builds your day around that.

regretnoun

what it costs you to have ignored something. Not how loudly it asked, and not when it arrived.

clientSwiftUI • iOS 26.4
serverFastAPI • Postgres
tests282 • 59 • 12/12
statusdeployed, running

// how it works

Twelve arrived. Four of them wanted something.

The same morning, twice. First as a list, in the order it landed. Then as a day, in the order it costs you — and the things that wanted nothing from you are not on it at all.

// what arrived • by time

// what it became • by consequence

Fig. 01

The model reads. Arithmetic scores.

Ask a language model for a number between 0 and 100 and you get a different answer on Tuesday than on Monday for the same message. So it is never asked. It extracts what it can genuinely judge — the stakes, the sender, whether there is a concrete task, when it is due — and four terms of arithmetic turn those into the score.

The regret function askcal-api/app/services/regret.py
consequencemoney_loss30
deadline≤ 96 h18
senderautomated_system6
actionrequired10
track weightfinance× 1.0
Regret 64
a concrete task, with stakes
confidence 0.91 ≥ 0.60
regret 64 ≥ 25

→ becomes a task on your day

Not a mock-up. The constants, both floors and the clamp are the ones in regret.py and config.py; this is that function, running here. Every message is invented — the shapes are real, the mail is not.

// what it refuses

Most mail is not work.

Scoring a message and making work out of it are different decisions. The first version of the second one was, in effect, “the model said yes” — five clauses, three of them almost always true, and a two-item blacklist that the loudest mail in any inbox walks straight through: a blast only has to be labelled money_loss to pass. Confidence and the score were both already computed and never once consulted. They are floors now.

Four gates, one morning askcal-api/app/services/autotask.py
the mail action consequence conf score verdict
50 new roles for you this weeknone0a digest is reading, not work
Someone viewed your profilesocial18social is never a task
Payment successful — card debitednone10money that already moved
Term fee — payable Fridaymoney_loss0.9164→ task, Thursday
Revision on the deck, by Thursdayclient_trust0.8885→ task, today
five to consider 0 became work

Two of five. The sender is deliberately not a gate: an assignment from a no-reply system is as real as a client writing by hand, and filtering automated senders out silently dropped exactly the work that mattered — a bug this project has shipped once and does not intend to ship twice.

// the arithmetic

Four terms, one multiplier, one dampener.

All of it, printed. A score you can print is a score you can argue with — and when something ranks oddly you read why in four lines rather than asking a model to explain a judgement it has already forgotten making.

Every constant in the formula askcal-api/app/services/regret.py • config.py

consequence

opportunity_loss
35
grade_loss
35
client_trust
35
money_loss
30
social
12
none
4

A missed assessment and a missed exam cost the same thing. Money is a shade below both — it is recoverable.

deadline • hours out

≤ 24, or overdue
40
≤ 48
28
≤ 96
18
≤ 168
10
beyond a week
4
overdue > 72 h
10
no deadline
0

Overdue lands in the hottest bucket and then cools, so a thing you missed last month stops pinning the day at maximum forever.

sender

client
12
recruiter
12
professor
8
automated_system
6
peer
4
other
0
newsletter
−20

Automated is worth points, not a penalty — assessment links and fee notices arrive from no-reply addresses exactly as noise does.

and the rest

action_required
10
per carry-forward
6
track weight
0.5–1.5
conf < 0.40
×0.6
task floor
25
conf floor
0.60
clamp
0–100

Carrying a task adds to it. The thing you keep pushing to tomorrow is usually the thing worth doing, and a score frozen on arrival can never say so.

// the day

Ranking is easy. Fitting it into a Thursday is not.

A ranked list is still a list. What survives the gates has to live in the hours you actually have.

Busy blocks come from your real calendar and are hard edges — the planner may not touch them and is never asked to negotiate. Work is placed highest-consequence-first into the earliest gap that fits it, so the brand deck outranks everything here and still starts at 11:30: the half hour before the pin could not hold ninety minutes, and something smaller took it instead.

A pin is an edge like any other. Nothing is ever placed into an hour that has already gone.

// in your words

A code review is work. It was filed under Design.

Askcal shipped with five categories baked into the database, the prompt and the app. They described a guess at somebody's life rather than anybody's actual one, and asked to pick one of five for a pull-request review, the model picked the closest — which was wrong. Tracks are rows you name now, and the sentence you write is what the classifier reads, over any assumption it has about what the name usually means.

Workweight 1.2 • active

Anything from my team or about a pull request, even when I'm not working.

makes tasks31 mails4 open
Feedweight 0.5 • active

Newsletters, digests, anything I read on a train and never act on.

never makes tasks212 mails0 open

the pull-request review now lands in Work • one edit • no migration, no deploy, no rebuild

// what it cost

The decisions worth defending.

Every one of these replaced something that already worked. That is the expensive kind.

01 • secrets

Encryption is a column type, not a call site.

A Google refresh token is a long-lived key to an entire mailbox and calendar. Encrypting it wherever it happens to be written means the one place that forgets writes plaintext into a column everything else assumes is safe. It is a database type instead, so forgetting is not on the menu. Stored values carry a version prefix, which is what let the key be rotated without taking a single mailbox offline.

02 • the classifier

Measured, not eyeballed.

Prompt changes used to be validated by waiting for real mail and seeing whether it looked right — which finds a regression days later, on the one message that mattered. A golden set of twelve cases, each carrying the rule it exists to pin, answers the same questions every time. Two of them found errors in the fixture before they found any in the prompt.

03 • the same bug, four times

State that decides what reaches you, applied invisibly.

A classifier silently unconfigured. An inactive track blocking every task under it. Carried work losing its day. Auto-task rules that only ever ran once, at classification, so turning a track on did nothing for mail already seen. Four disguises, one shape — and everything added since has to answer for it.

04 • migrations

Every step could be undone.

Three schema changes each added the new shape beside the old one and backfilled, so every step deployed alone and could roll back. The columns came out much later, in a migration whose own docstring admits it restores the shape and not the data.

05 • the one that got away

A dropped column, and a 500 on every request.

One migration dropped a column the model still declared. Every authenticated request failed for as long as it took to notice, which was an evening. The test suite was green throughout, and correctly so — it is deliberately database-free, which is what makes it fast enough to run on every change and also what makes model-versus-schema drift invisible to it. There is a command that catches exactly this. It is not in CI yet.

06 • notifications

A push that says nothing teaches you to swipe it away.

The morning digest and the evening nudge fired fixed text — “your day is ready” — which is a reminder that something exists, not a reason to open it. Both are written server-side now, in the same call as the screen behind them, because a push promising three deadlines that opens onto a screen showing two is worse than no push at all.

// the whole of it

Stop asking what to do next. Let the cost decide.

Read the source

Askcal is not a product and there is nothing to sign up for. It runs, it is deployed, and the code is open.