Send later for drafts
Let users schedule a draft to be sent at a chosen time. Scheduled drafts are stored server-side and sent by the existing job runner, so closing the app does not cancel them.
4 decisions for you; 1 asked and answered in Codex, recorded in the ledger; 2 defaults the plan assumed, kept unless you change them below
QuestionAnswered in Codexn-send-later-entry
Send later entry
Where should users schedule a draft?
Default: Menu next to Send · leave it empty and it is recorded as "not answered; default kept", not as agreement
Questiond1
Where is the schedule stored?
Default: column on the drafts table · leave it empty and it is recorded as "not answered; default kept", not as agreement
Affects:
src/db/migrations/**src/api/drafts.tsQuestiond2
What happens if sending fails at the scheduled time?
Default: Retry 3 times with backoff, then mark as failed · leave it empty and it is recorded as "not answered; default kept", not as agreement
Affects:
src/jobs/sendScheduled.tsQuestiond3
Maximum schedule horizon?
Default: 30 days · leave it empty and it is recorded as "not answered; default kept", not as agreement
Questiond4
Time zone for the picker
Time zone for the picker: use the user's profile time zone or the device time zone?
No default, please choose · leave it empty and it is recorded as "not answered; default kept", not as agreement
Affects:
src/ui/composer/**AssumptionDefault, reviewabled-drafts-without-recipients-cannot-be
Drafts without recipients cannot be scheduled.
Default: Drafts without recipients cannot be scheduled. · leave it empty and it is recorded as "not answered; default kept", not as agreement
AssumptionDefault, reviewabled-default-horizon-is-30-days-unless-d3
Default horizon is 30 days unless D3 says otherwise.
Default: Default horizon is 30 days unless D3 says otherwise. · leave it empty and it is recorded as "not answered; default kept", not as agreement
Full plan
Send later for drafts
Summary
Let users schedule a draft to be sent at a chosen time. Scheduled drafts are stored server-side and sent by the existing job runner, so closing the app does not cancel them.
Key Changes
-
Add
scheduled_atto the drafts table and expose it onPATCH /drafts/:id(src/api/drafts.ts). -
New job
send-scheduled-draftsinsrc/jobs/sendScheduled.ts, polled every minute by the job runner. -
Composer gets a "Send later" menu next to Send (
src/ui/composer/SendButton.tsx).
Decisions
-
D1: Where is the schedule stored?
- Option A: column on the drafts table (Recommended)
-
Option B: separate
scheduled_sendstable -
Affects:
src/db/migrations/**,src/api/drafts.ts
-
D2: What happens if sending fails at the scheduled time?
- Retry 3 times with backoff, then mark as failed (Recommended)
- Mark as failed immediately and notify the user
-
Affects:
src/jobs/sendScheduled.ts
-
D3: Maximum schedule horizon?
- 30 days
- 1 year
- Recommended: 30 days
-
D4: Time zone for the picker: use the user's profile time zone or the device time zone?
-
Affects:
src/ui/composer/**
-
Affects:
Test Plan
-
Unit: job picks only drafts with
scheduled_at <= nowand not yet sent. - Integration: schedule, restart the job runner, draft still sends once.
- UI: menu hidden when the draft has no recipients.
Assumptions
- Drafts without recipients cannot be scheduled.
- Default horizon is 30 days unless D3 says otherwise.