Repository navigation
Open
Labels
bugSomething isn't workingSomething isn't workingseverity: mediumNotable gap with a workaround, or competitive parityNotable gap with a workaround, or competitive parity
Description
Follow-up to #154 / #155. #155 makes a permanent render error pause the sequence (reason render_error) and keeps everyone queued. That is the send-time backstop. The usual industry approach is to stop these problems before a send starts, and to handle per-recipient failures without losing anyone. This issue tracks that work plus the existing issues found while reviewing #155.
Prevention (main item)
- Validate Liquid syntax when content is saved. Sequence emails, broadcasts and templates are only scanned for tokens with a regex (
templates/validation.ts). Content like{{ nameor{% foo %}is accepted and only fails at send time, where fix(api): sequences stall on wrapped footer address; render failures retry forever #155 now pauses the send. Reject it on save with the line and column from liquidjs. - Guard the mailing address. Activation requires one, but it can be cleared later in Settings while broadcasts or sequences are active. Every send then fails with
footer_render_context_required. Block blanking it while sends are active, or warn and list what will pause. - Pre-send checklist. Consider one activation-time check covering footer, mailing address, unsubscribe link, merge-tag syntax and delivery source, so problems show up before any recipient is enrolled.
Per-recipient failures
- If a render error depends on one contact's data (for example strict Liquid variables, or filters on contact fields), the standard is to skip that recipient's email and record the reason, not pause the whole send or retry. Today this is unlikely because Liquid runs in non-strict mode, but
isPermanentRenderErrortreats everyLiquidErroras send-wide.
Existing issues found during review
- Lost updates to
sequences.report. On the send-error path,report.sequence.failedis written read-modify-write from the sequence snapshot loaded when the job started. The sequence worker runs 10 jobs at once, so concurrent failures overwrite each other and can also dropreport.health. Use an atomicjsonbappend. retry_countnever resets after a successful send. Failures add up across all steps of a multi-email drip, so a few unrelated failures months apart can drop a contact.- A broadcast never completes if its last rows are dropped at the bounce limit. The row is deleted and the error rethrown, so
cleanUpResourcesandmarkBroadcastSentnever run and the broadcast stays in its sending state. - Subscribers who join while a sequence is auto-paused are never enrolled.
autoPauseSequenceremoves the trigger rule, so a "new contact subscribes" welcome sequence silently misses everyone who subscribes until it is resumed. Consider enrolling them as queued.
Smaller items
renderErrorReasoncopy is duplicated inapps/api/src/automation/sending-health.tsandapps/web/components/dashboard/auto-pause-banner.tsx. Share it if more reasons are added.sequenceBounceLimitis used for all send failures, not just bounces; the name is misleading.
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't workingseverity: mediumNotable gap with a workaround, or competitive parityNotable gap with a workaround, or competitive parity
Type
Projects
- StatusShow more project fieldsBacklog
Activity
s3rot0n1n commentedon Oct 29, 2026
More items from the review of #155:
{% if subscriber.name %}{% include x %}{% endif %}, only fails for some contacts. fix(api): sequences stall on wrapped footer address; render failures retry forever #155 renders the email once without contact data to tell content errors from data errors, and pauses afterAUTO_PAUSE_CONSECUTIVE_FAILURESconsecutive skips. If the affected contacts are interleaved with ones that send fine, they are still skipped (recorded inreport.sequence.failed) with no pause or notification.include,renderandlayoutcan never work in SendLit because no template root is configured. Rejecting them on save, or making them parse errors in the engine, would close this.{{ x) passes the footer check, because the check runs before Liquid. Liquid then fails, so the owner is told the email's content couldn't be built. The address should be escaped or kept out of Liquid parsing. Today an address such as{{ subscriber.email }}would also render as template code.report.sequence.failedentries. A contact skipped on several emails of a sequence is listed once per email. A crash between recording the skip and advancing the row lists them twice.