You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: scripts/loops/README.md
+8-4Lines changed: 8 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -80,21 +80,25 @@ Deploy the generated schema before code that selects the new columns. The unship
80
80
81
81
With the schema available and test configuration set, `bun scripts/loops/seed.ts` reports how many completed signups would be queued. `--apply` queues them without sending anything. `bun scripts/loops/sync.ts --apply` processes a bounded batch using the same worker as the cron route. Test mode fails closed without an allowlist. Set `LOOPS_SYNC_MODE=production` only as part of the reviewed cutover. Seed old completed accounts once after the final suppression reconciliation; subsequent signups enter the queue through the application.
82
82
83
+
Sync fingerprints include the contact's effective signup and teammate-join eligibility, rather than the raw enrollment switch or cutoff date. Enabling enrollment or moving a future cutoff does not invalidate every historical profile. A signup or join crossing the cutoff, or disabling an eligible contact's enrollment, still changes its fingerprint and requires synchronization.
84
+
83
85
## Independent delivery check
84
86
85
87
`.github/workflows/loops-safety.yml` runs every five minutes, independently of the Cap cron worker. It is disabled until the repository variable `LOOPS_WATCHDOG_ENABLED` is explicitly set to `true`. Configure GitHub secrets `LOOPS_API_KEY` and `LOOPS_HEALTH_SECRET`, and configure the same health secret in Cap. Scheduled Actions run from the default branch, so this protection is not deployed while the PR remains unmerged.
86
88
87
89
The authenticated, read-only `/api/cron/sync-loops/health` endpoint reports queue counts without contact data. It reports unhealthy when production sync or enrollment is disabled, the queue is empty, a due job is over five minutes late, or a job has failed three times. Expected holds for incomplete signup and removed/changed identities do not count as delivery failures. An unchanged successful refresh clears old failures; a newly queued change does not inherit an older attempt's retry delay.
88
90
89
-
The checker rejects unavailable, malformed or stale health responses. On failure or audience drift it adds mutually exclusive subscription conditions to each journey's first audience guard, with scope set to all following nodes, then reads the guards back. One failed update does not stop attempts on the other journeys. An update failure or existing hold fails the Action so it remains visible. Assign an owner to GitHub Actions failure notifications and verify that notification before activation.
91
+
The checker rejects unavailable, malformed or stale health responses. For draft or paused journeys, it can add mutually exclusive subscription conditions to the first audience guard, with scope set to all following nodes, then read the guards back. One failed update does not stop attempts on the other journeys. An update failure or existing hold fails the Action so it remains visible.
92
+
93
+
For a running journey, Loops rejects audience edits with HTTP 400: "This operation is not allowed while the workflow is sending." The checker reports `manual-pause-required`, fails the Action, and explicitly warns that delivery has not stopped. Pause the affected workflows in the Loops UI before applying a hold or changing their guards. This monitor does not automatically stop active delivery. Assign an owner to GitHub Actions failure notifications and verify that notification before activation; do not launch on an assumption of automatic pausing.
90
94
91
95
The hold persists after recovery. It does not change workflow status, contact subscriptions or campaign schedules. The current four production drafts have this hold applied and independently verified. To inspect them without writes:
92
96
93
97
```sh
94
98
bun run emails:check-loops --structure-only --require-held
95
99
```
96
100
97
-
For an emergency hold, with the Loops API key in the environment:
101
+
For an emergency, first pause active workflows in the Loops UI. Then, with the Loops API key in the environment, apply persistent audience holds to the stopped workflows:
98
102
99
103
```sh
100
104
bun scripts/loops/watchdog.ts --hold --apply
@@ -108,7 +112,7 @@ bun scripts/loops/watchdog.ts --resume --apply
108
112
109
113
Resume requires a fresh authenticated health response and refuses changed audience rules. It does not start a draft or unpause a workflow. Contacts that have already exited because of a hold are not automatically replayed; assess recovery separately without bulk re-enrolling history.
110
114
111
-
GitHub scheduling can be delayed, and a Loops API outage can prevent guard updates. A message already being sent may still arrive. Test a running owned-account journey across an outage in the deployed environment before relying on this protection. Manually pause Loops if the check cannot apply a hold. Campaigns require a fresh health check before scheduling and manual cancellation or pause during an outage; the checker only guards the four registered journeys.
115
+
GitHub scheduling can be delayed, and a Loops API outage can prevent even stopped-workflow guard updates. Running workflows keep sending until they are manually paused; a message already being sent may still arrive. Verify the alert and manual-response procedure with an owned-account journey before relying on it. Campaigns require a fresh health check before scheduling and manual cancellation or pause during an outage; the checker only inspects the four registered journeys.
112
116
113
117
## Before any activation
114
118
@@ -125,7 +129,7 @@ Activation is deliberately outside this migration's approved scope. These gates
125
129
1. Review the final custom drafts and audiences, and configure rotated credentials in the intended environment. September 11 controlled deliveries passed SPF, DKIM and DMARC with inherited `p=quarantine`; the earlier DMARC warning is no longer an observed blocker. Recheck sending-domain status at cutover.
126
130
2. The native Stripe connection passed owned live-customer creation/update tests, including name sync, list assignment, global unsubscribe preservation and a recipient Preference Center mailing-list opt-out surviving a later native update. Import/reconcile the remaining Bento negative records with workflows off. Then deploy the reviewed schema/code in test mode and prove the actual signup, purchase and invite routes with owned accounts.
127
131
3. Set a future `LOOPS_ENROLLMENT_AFTER` timestamp, verify the cron schedule and permissions in the deployed environment, and seed the completed-account sync jobs. Keep enrollment disabled while inspecting the resulting contacts. Preserve existing opt-outs and suppressions; no separate consent-capture step is needed.
128
-
4. Configure and enable the independent delivery check above, verify its deployed health endpoint and failure notifications, and prove purchase/invite/opt-out transitions remove contacts before later promotional steps. Exercise its hold and explicit recovery with an owned-account journey. Downstream filters use the last synced fields; `capVerifiedAt` is not a native expiry guarantee. Pause workflows manually if the checker cannot reach Loops, and handle scheduled campaigns separately.
132
+
4. Configure and enable the independent delivery check above, verify its deployed health endpoint and failure notifications, and prove purchase/invite/opt-out transitions remove contacts before later promotional steps. Exercise failure detection, manual pause, hold and explicit recovery with an owned-account journey. The September 11 live test confirmed that the API cannot change guards while Sending; automatic stopping remains unresolved. Approve an operational plan with manual pausing, or keep delivery off until a supported automatic control is available. Downstream filters use the last synced fields; `capVerifiedAt` is not a native expiry guarantee. Handle scheduled campaigns separately.
129
133
5. Reconcile a fresh Bento delta at cutover, including all opt-outs and changed entitlements. Confirm no campaign/flow is queued to send twice, check overlap with Resend recording emails, then disable old Bento marketing automations only as part of the approved cutover. Preserve source history and suppression evidence.
130
134
6. Test the deployed Cap signup/purchase/invite/opt-out path through Loops to an owned inbox, including a sync outage and retries. The completed synthetic profile-to-Loops tests do not replace this production integration check.
131
135
7. After explicit activation approval, choose a future enrollment boundary with enough setup time. While that boundary is still in the future, enable production sync/enrollment and the independent delivery check, confirm healthy queue results, explicitly remove the delivery holds, and start the reviewed Loops workflows. Complete those steps before the boundary so the first eligible signup is processed by an already-running workflow; if setup overruns, move the boundary forward before allowing enrollment. This release admits new signups after the boundary, not a numerically limited cohort. Monitor deliveries, complaints, opt-outs and duplicate suppression. Do not bulk enroll imported history or replay stage changes emitted while workflows were drafts or held. Rollback stops new enrollment and pauses Loops before considering re-enabling Bento; never run both senders for the same journey. Retire Bento and rotate remaining credentials after reconciliation.
0 commit comments