Summary
#977 fixed the visibility symptom for deferred scheduled sends, but it exposed a deeper product question:
When a scheduled send cannot be submitted at its requested send_at because of the daily cap, should e2a silently send it later?
The current contract describes send_at as "not before," so silent deferral is technically consistent with the current behavior. The question is whether that is the right product behavior when timing is explicitly requested.
Current behavior
At fire time, the per-account daily send cap can prevent submission. Today the worker:
- releases the send claim (
delivery_status returns to accepted);
- snoozes the job to the next UTC midnight; and
- leaves
scheduled_at unchanged.
As a result, the message is silently deferred to a later window without the requester being told that the requested time was missed.
The visibility consequence of this was fixed in #977: an accepted message with a past scheduled_at now remains visible as Overdue. This issue is about the underlying send behavior, not the dashboard visibility.
Product question
There are three possible behaviors:
A — Best-effort / silent defer
Keep the current behavior: if the daily cap prevents sending, automatically retry in the next available window.
Pros
- Preserves the current "not before / best-effort" contract.
- No additional interaction required.
Cons
- The actual send time can differ significantly from what was requested.
- The requester has no way to know that the requested time was missed.
B — Require rescheduling
If the requested time cannot be honored, surface that explicitly and let the requester choose a new time or cancel.
Pros
- Never changes an explicit requested time without consent.
- Makes timing failures explicit.
Cons
- Adds friction and requires a mechanism for the requester to respond.
- For autonomous agents, this needs to be handled through an event/API rather than only through the dashboard.
C — Observable best-effort
Keep automatic deferral, but make it observable through a distinct state and/or event.
For example:
- Dashboard shows the send as deferred/overdue with the reason.
- A webhook/event notifies the requesting agent.
- The operator or agent can then reschedule, cancel, or accept the deferred send.
This preserves best-effort delivery while removing the "silent" behavior.
Agent-first consideration
Because the requester is often an autonomous agent, the general mechanism should likely be state + event, rather than a dashboard-only interaction.
For example, an email.deferred or send.overdue event could allow:
- a human operator to see and act on it in the dashboard;
- an agent to decide programmatically via webhook/SDK/MCP.
This seems consistent with e2a's existing event-driven model.
Possible data-model implication
Depending on the decision above, we may also need to distinguish:
- requested send time — what the requester specified (
scheduled_at);
- next attempt time — when the system will actually retry.
Today the latter exists only in the River job and is not exposed through the API.
If we choose an observable-defer model, this could eventually warrant a next_attempt_at field and/or a distinct deferred / needs_reschedule state.
Scope
In scope
- Daily-cap behavior for scheduled outbound sends.
- The
send_at contract.
- Any state/event needed to make deferral observable.
- How the behavior is surfaced through dashboard, SDK/MCP, and webhook.
Out of scope
References
Summary
#977 fixed the visibility symptom for deferred scheduled sends, but it exposed a deeper product question:
When a scheduled send cannot be submitted at its requested
send_atbecause of the daily cap, should e2a silently send it later?The current contract describes
send_atas "not before," so silent deferral is technically consistent with the current behavior. The question is whether that is the right product behavior when timing is explicitly requested.Current behavior
At fire time, the per-account daily send cap can prevent submission. Today the worker:
delivery_statusreturns toaccepted);scheduled_atunchanged.As a result, the message is silently deferred to a later window without the requester being told that the requested time was missed.
The visibility consequence of this was fixed in #977: an accepted message with a past
scheduled_atnow remains visible as Overdue. This issue is about the underlying send behavior, not the dashboard visibility.Product question
There are three possible behaviors:
A — Best-effort / silent defer
Keep the current behavior: if the daily cap prevents sending, automatically retry in the next available window.
Pros
Cons
B — Require rescheduling
If the requested time cannot be honored, surface that explicitly and let the requester choose a new time or cancel.
Pros
Cons
C — Observable best-effort
Keep automatic deferral, but make it observable through a distinct state and/or event.
For example:
This preserves best-effort delivery while removing the "silent" behavior.
Agent-first consideration
Because the requester is often an autonomous agent, the general mechanism should likely be state + event, rather than a dashboard-only interaction.
For example, an
email.deferredorsend.overdueevent could allow:This seems consistent with e2a's existing event-driven model.
Possible data-model implication
Depending on the decision above, we may also need to distinguish:
scheduled_at);Today the latter exists only in the River job and is not exposed through the API.
If we choose an observable-defer model, this could eventually warrant a
next_attempt_atfield and/or a distinctdeferred/needs_reschedulestate.Scope
In scope
send_atcontract.Out of scope
References
internal/agent/outbound_async.go— daily-cap deferral path.internal/httpapi/outbound.go— currentsend_atcontract.internal/outboundsend— outbound send/retry behavior.