Report Subscription
Report Subscription = automatic subscription to receive recurring reports (for example a weekly report). It is a focused automation path that runs through the core primitives:
AutomationScheduler, shared key helper,Ensure*, dispatch guard, send claim.
Path
Sending
OutgoingMessagedirectly while skipping theNotificationSchedulerbridge requires separate approval and still must keep the send-claim guard.
Use AutomationScheduler (no new columns)
AutomationTypeId = ReportSubscription
TriggerTypeId = ScheduledRun | ManualRun
SourceEntityTypeId = ReportSubscription ; SourceEntityGuid = ReportSubscription.Guid
SchedulerKey = report-subscription:{guid}:scheduled:{periodStartUtc-or-scheduledOn}
| report-subscription:{guid}:manual:{manualRunGuid}SchedulerKeyis the canonical business run key — do not derive delivery keys fromAutomationScheduler.Id/Guid.manualRunGuidis generated before insert, returned to the caller, and reused on retry.StorageRunKeyHash(stable short hash ofSchedulerKey) groups delivery artifacts (MessagingJob.Guid, reportNotificationJob.RowKey).
One-Owner Guard — no double send during cutover
Old scheduler and new automation scheduler both check an ownership guard before creating delivery:
ReportRunOwnership: SchedulerKey, OwnerTypeId (OldScheduler | AutomationScheduler),
OwnerVersion, StatusId (Owned | DeliveryCreated | Cancelled), CoordinatesJson?- Only the owner may create
NotificationJob/MessagingJob/blob/NotificationScheduler. - Shadow rows do not acquire ownership.
- Retry reuses ownership + coordinates. Ownership conflict →
IsOnSupportHold, never creates a second delivery.
Step-by-step rollout (safe, rollbackable)
0 Design validation : confirm delivery path, old scheduler behavior, and storage ownership.
1 Shadow : non-running row; compare the expected key with the old scheduler run.
2 Run-now allowlist : automation runs selected manual runs; the old scheduler is blocked for them.
3 Scheduled allowlist: choose BU/subscription; monitor conflict/failure/latency.
4 Cutover : guard the old scheduler for subscriptions in the allowlist;
rollback = remove the allowlist; ownership returns to the old scheduler.Lifecycle & support
- Create/update → ensure next scheduled row (only next run, unless a clearly bounded horizon is needed).
- Pause/deactivate/delete → deactivate pending rows; completed runs are kept.
- Template/recipient/delivery changes → reload at execution. Run-now → new
manualRunGuid. - Support API:
GET/POST /report-subscriptions/{guid}/automation-runs[...]— report run detail, ownership detail, delivery coordinates, failed report queue, recent runs/subscription.
Related
- Automation & Reminders — shared automation foundation.
- Subscription — different from "report subscription"; subscription is a learning/billing package.
Current run contract
The implementation supports create/edit, pause/resume, delete, duplicate and Run Now. Every run stores the generated report and exposes authorized view/download links; attachments are optional, and an oversized attachment falls back to link-only delivery. Run history records scheduled/manual origin, recipients and delivery status. Scheduler claiming is concurrency-safe, and manual export uses the same report service path.
Real-world example: A scheduled Roll Sheet is too large to attach. → Store the file and send a secure link instead. → The run history records success with link-only delivery.