Consumer App + Agent Platform
Procrastitask
A reminder fires once, usually at the worst possible moment, and you swipe it away. The task is still undone and nothing follows up. Procrastitask keeps going: a quiet nudge, then a firmer one, and for anything marked must-not-miss, a full-screen alarm that gets past Do Not Disturb. The clock lives on the server, so closing the app changes nothing.
- FastAPI + Postgres on AWS App Runner
- Expo / React Native, iOS-first
- Native iOS 26 AlarmKit module
- Hosted MCP server, 15 tools
Overview
Procrastitask is a consumer iPhone app for the obligations you keep putting off. It does two things no incumbent does together: it escalates a reminder until the task is closed, and it lets a partner or roommate own the task so the app does the nagging instead of a person. I built all of it: the escalation engine, the API, the client, the native alarm module, the brand, the store listing, and the launch.
- The server owns the clock: a per-minute sweep drives every timeline, so the phone is never the timer. Close the app or restart the device and the ladder still advances
- Escalation is completion-gated: nothing expires quietly. A task stops nagging when it is done, snoozed, or explicitly dropped, and completing it pulls back every pending alert
- Sharing requires consent: an assigned task is a request until the owner accepts it, so nobody gets nagged by a task they never agreed to
- Agents get a real surface: a hosted MCP server with 15 tools lets a user point Claude Code at their own account and create, edit, and close tasks as themselves
Problem
The reminder market splits into two camps, and nobody has merged them. The apps that nag you well cannot share at all. The ones that share remind everybody, which holds nobody to anything.
Fires once, then gone
- A calendar alert or a single ping is swiped away in a second
- Nothing keeps an open obligation loud until it is closed
- Digests and recaps get dismissed reflexively
Sharing without ownership
- Shared lists show who created a task, not who owns it
- They remind everyone, which means they remind nobody
- Google Tasks has no cross-account sharing primitive at all
No signal on a miss
- Persistence apps like Due nag hard and have no sharing
- Galarm has the buddy loop but is an ad-supported alarm clock
- So the accountability falls back to a manual "did you do that thing?" text
The gap is narrow and specific: owner-assigned tasks, escalating nags aimed only at the owner, and a signal back to the assigner on a miss or a completion, inside something that behaves like a task manager rather than an alarm clock.
The escalation ladder
The engine is ported from Hestia, my own assistant system, where the three-tier ladder had already been running against real tasks before any of this was a product. A task that passes its due time enters the ladder, rises with how long it has been overdue, and rolls forward at the top tier until someone closes it.
| Stage | What happens |
|---|---|
| Pre-due | Deadline-style tasks get sparse, gentle heads-up nudges inside a lead window, with a clean handoff so they never suppress the first real nag at due time |
| Stage 0 | A standard notification at due time, fired exactly once |
| Stage 1 | Time-Sensitive notifications on a repeating cadence, gated on the last nudge so nothing floods |
| Stage 2 | Rolls forward indefinitely. On tasks marked must-not-miss, this is a native iOS 26 AlarmKit alarm that pierces silent mode and Focus |
| Shared escalation | The assigner is pinged when an owned task slips, and again when it is finally done |
Every stage is per-task adjustable, and the whole ladder is instrumented on nudges-per-completion so the defaults can be tuned from real behavior.
The parts that took the longest
- Two modes. Every task is either
escalateornotify_once. An informational reminder fires once and closes. Applying persistence indiscriminately is the single loudest complaint against the apps in this category, and it is how you get uninstalled in a week - Snooze is a real state. Snoozing resets the ladder to stage 0 and preserves the due date, so escalation resumes gently rather than picking up where it left off
- Silencing has to equal closing. AlarmKit does not fire its Stop intent on a swipe-away, so acknowledgement is routed through custom Done and Snooze buttons. A swipe is treated as "not done, keep going"
- Escalation advances on absence. iOS gives no guaranteed delivery receipt, so the ladder never waits for a positive "delivered" signal that does not exist. It advances on the absence of a completion
Agent-native access
Plenty of reminder apps have an API. Almost none are built for someone else's agent to drive. Procrastitask ships a hosted MCP server as a product surface of its own, so a user can point Claude Code at their own account and have it file and close obligations on their behalf.
claude mcp add --transport http procrastitask \ https://api.procrastitask.app/mcp \ --header "Authorization: Bearer pk_..."
One command connects an agent. It then acts as the token's user, on that user's own tasks only.
- 15 tools covering create, read, update, complete, snooze, skip, drop, escalation policy, stats, settings, and pause
- Mounted into the same app at
/mcpover streamable HTTP, wrapping the same service layer as REST so there is one code path and one set of invariants - Lossless round-trips: reads return the fuller task shape, so an agent that creates a task and reads it back sees everything it set
- Personal access tokens that are hashed at rest, scoped to
tasks:read/tasks:write/settings:write, and revocable from the phone - Own-tasks-only. An agent cannot reach another member of a shared space, so a compromised token cannot nag someone else
- Session tokens are rejected on the MCP surface. An agent never carries the credential a human signed in with
An agent can take an obligation off your plate, but a person still has to do the thing. The escalation engine is what turns an agent's reminder into follow-through.
Architecture
Python 3.12 and FastAPI on AWS App Runner, backed by Neon Postgres, behind a branded API domain. The client is Expo and React Native with one Swift native module for AlarmKit.
Pure engine, testable
- Escalation and recurrence are database-free modules
- Unit-tested in isolation against clock inputs
- The hard logic never needs a live Postgres to prove
The sweep
- Per-minute worker holds an advisory lock, so two instances never double-send
- Batch loads replaced the N+1 that shipped first
- All-day tasks pin to 09:00 local rather than midnight
Cancel everywhere
- Completing a task halts the timeline server-side
- An APNs collapse ID pulls back pending alerts on the device
- Recurring tasks roll to the next occurrence with the ladder reset
Auth
- Sign in with Apple or a passwordless email code
- 90-day session tokens for humans, 365-day PATs for agents
- Both hashed; write scope enforced centrally on any non-GET
Schema discipline
- Alembic runs in the container entrypoint before the app starts
- A test fails the build on any model-to-migration drift
- The deploy self-adopted the pre-Alembic database by stamping a baseline
Observability
- Structured JSON logs with a request ID through every layer
- APNs errors classified rather than swallowed
- Rate limiting hardened on the public auth surface
A green health check is not proof the app is whole
The Docker image installed dependencies unpinned at build time, so its contents were a function of the build date rather than the commit. When a dependency moved an import in a major release, the MCP mount failed inside a caught ImportError and agent access went down for six days while /health stayed green the whole time. The fix was three-part: lock every dependency so the image follows the commit, refuse to boot in the cloud when the mount fails, and smoke-check the actual /mcp surface on every deploy. Probe the feature, not the process.
Working when the app is closed
The home-screen widget and the Live Activity stayed current only for people who opened the app most days. The widget snapshot was written by a foreground sync, with a background task as the sole fallback, and iOS budgets those by how much you use the app. The server could start a Live Activity but had no way to update or end one. Both are the wrong failure mode for an app whose promise is that it keeps working when you are ignoring it.
- Every push now carries the snapshot. A Notification Service Extension writes it into the shared App Group with no app runtime and no background budget
- Live Activity update and end pushes ramp a card through the stages and dismiss it on completion, using a per-activity token distinct from the one that starts it
- The widget builds a multi-entry timeline, so a task appears at its due minute with no push involved at all
- A silent push covers the rest: changes with no alert to ride on, like a task created on another device or filed by an agent over MCP
Getting the alarm right
A real user got a full-screen breakthrough alarm at 1am for an ordinary task. An app that does that deserves to be deleted, so it got a three-auditor adversarial pass rather than a patch.
The audit cleared the server: the ladder, the notify-once mode, the assigner loop scoped to the assigner's own timezone, and the daylight-saving math were all sound. The gap was on-device alarm invalidation, which is a different bug than the one the symptom pointed at.
- The breakthrough alarm is now opt-in per task via must-not-miss. An ordinary escalating task tops out at the quiet-hours-gated push ladder and never rings
- Editing or downgrading a task tears down a stale on-device alarm, from the server and again from a client-side reconciliation pass
- Quiet hours default to 23:00 through 09:00 and are on by default. Only the top stage can break them, and only when the user opts a specific task in
- Global pause gives a time-boxed mute for surgery, illness, or a real holiday. Paused time is a third outcome, so it touches neither wins nor misses
Must-not-miss tasks keep ringing through a pause by default, because the reason someone pauses is usually medical and the thing they marked must-not-miss is usually medication.
Shipping it
I do not own a Mac. Every iOS build runs in the cloud on EAS and installs on a physical iPhone, which rules out the simulator as a debugging surface and makes each build expensive enough that batching them is a real constraint on how the work is sequenced.
Review, twice
- Rejected under 3.1.2(c) for a missing Terms of Use link in store metadata
- Rejected under 2.1(b) because the in-app purchases were created but never attached to the submission
- Neither required a new binary, and both are invisible until Apple names them
Listing as code
- Title, subtitle, description, and keywords pushed through the App Store Connect API from a checked-in config
- Framed marketing screenshots rendered from a seeded demo account
- TestFlight external beta with a public link, created the same way
Release control
- Manual release, so approval parked the build instead of publishing it
- Pre-launch accounts grandfathered by user ID before the paywall flip
- Entitlements written by one webhook, so the server is the only source of truth on who is Pro
CI that fails honestly
For most of the build there was no pull-request CI while main auto-deployed to production. Every pull request now runs lint, the full server suite against a Postgres service container, and a mobile typecheck. The suite skips database-backed tests when Postgres is unreachable, so the workflow asserts connectivity and fails on any skip. A green run that quietly skipped everything is worse than no CI at all.
Outcomes
Delivered capabilities
- Server-owned escalation engine with per-task modes and overrides
- Native iOS 26 AlarmKit breakthrough alarm, opt-in per task
- Shared spaces, single-use invites, consent-gated assignment
- Assigner loop on miss and on completion, timezone-correct
- Natural-language quick entry for due dates and recurrence
- Recurring tasks with multi-weekday rules and end conditions
- Hosted MCP server and public OpenAPI over one service layer
- Scoped, revocable personal access tokens managed in-app
- Widget and Live Activity that stay current with the app closed
- Global pause that leaves streaks and miss counts untouched
- Own-APNs delivery plus an FCM transport for Android
- Design-token system driving both light and dark themes
Building something agents can actually drive?
Procrastitask is what it looks like to treat an agent as a first-class client of a consumer product: one service layer, one set of invariants, scoped tokens a user can revoke, and a human-facing app that does not degrade because the agent surface exists. If you are putting an MCP surface on a real product, I would like to hear about it.