knadh/listmonk · error
campaigns.fieldInvalidSendAt
Error message
campaigns.fieldInvalidSendAt
What it means
When the campaign has a send_at date set (c.SendAt.Valid), validateCampaignFields requires it to be strictly in the future: if c.SendAt.Time.Before(time.Now()) it returns campaigns.fieldInvalidSendAt. The library refuses to schedule a campaign in the past.
Source
Thrown at cmd/campaigns.go:714
}
// If no content-type is specified, default to richtext.
if c.ContentType != models.CampaignContentTypeRichtext &&
c.ContentType != models.CampaignContentTypeHTML &&
c.ContentType != models.CampaignContentTypePlain &&
c.ContentType != models.CampaignContentTypeVisual &&
c.ContentType != models.CampaignContentTypeMarkdown {
c.ContentType = models.CampaignContentTypeRichtext
}
if c.ContentType != models.CampaignContentTypeVisual {
c.BodySource.Valid = false
}
// If there's a "send_at" date, it should be in the future.
if c.SendAt.Valid {
if c.SendAt.Time.Before(time.Now()) {
return c, errors.New(a.i18n.T("campaigns.fieldInvalidSendAt"))
}
}
if len(c.ListIDs) == 0 {
return c, errors.New(a.i18n.T("campaigns.fieldInvalidListIDs"))
}
if !a.manager.HasMessenger(c.Messenger) {
// If it's a specific SMTP, but it's no longer available (removed/disabled), fall back to general email messenger.
if strings.HasPrefix(c.Messenger, "email-") {
c.Messenger = "email"
} else {
return c, errors.New(a.i18n.Ts("campaigns.fieldInvalidMessenger", "name", c.Messenger))
}
}
camp := models.Campaign{Body: c.Body, TemplateBody: tplTag}
if err := c.CompileTemplate(a.manager.TemplateFuncs(&camp)); err != nil {View on GitHub (pinned to 670c01717d)
Solutions
- Generate send_at at the last possible moment before the request using UTC RFC3339 and verify it is > time.Now().
- If you don't want scheduling, omit send_at / send null so SendAt stays invalid (unset).
- Update the campaign to a new future date if the original slot has already passed.
- Account for timezone: send full ISO-8601 with offset instead of naive local datetime strings.
Example fix
// before
{"send_at": "2026-01-01T09:00:00Z"}
// after
{"send_at": time.Now().Add(30 * time.Minute).UTC().Format(time.RFC3339)} Defensive patterns
Strategy: validation
Validate before calling
function assertFutureSendAt(sendAt) {
if (sendAt == null) return; // unset is fine
const t = new Date(sendAt);
if (isNaN(t.getTime()) || t.getTime() <= Date.now()) {
throw new Error('send_at must be a valid RFC3339 date in the future');
}
} Type guard
function isFutureDate(v: unknown): v is string {
if (typeof v !== 'string') return false;
const t = new Date(v);
return !isNaN(t.getTime()) && t.getTime() > Date.now();
} Try / catch
try {
await api.createCampaign({ send_at: sendAt, ...rest });
} catch (e) {
if (e.message.includes('fieldInvalidSendAt')) {
throw new Error(`send_at ${sendAt} is in the past relative to the server clock`);
}
throw e;
} Prevention
- Always send RFC3339/ISO-8601 timestamps with explicit UTC offset.
- Compute send_at at request time, never cache it in long-running jobs.
- Add a small safety margin (e.g. now + 1 minute) for clock skew.
- Omit send_at for immediate sends.
When it happens
Trigger: CreateCampaign or UpdateCampaign with a send_at timestamp earlier than the server's current time, e.g. sending a RFC3339 string generated earlier or in a timezone that resolves before server 'now'.
Common situations: Clock skew between client and server, client generating the timestamp in a different timezone (naive local time parsed as UTC), or retrying a stale payload minutes later so the date is now past.
Related errors
- campaigns.fieldInvalidFromEmail
- campaigns.fieldInvalidName
- campaigns.fieldInvalidSubject
- campaigns.fieldInvalidListIDs
- campaigns.fieldInvalidBody
AI-assisted analysis of knadh/listmonk@670c01717d (2026-09-01).
Data as JSON: /api/errors/770fbe33f76c30f8.
Report an issue: GitHub.