---
id: CAP-PFM-QRY-015
title: Deliver recurring query and report files
feature: FEAT-PFM-QRY-05
product_area: platform
module: queries
revision: 1
status: draft
product_intent: confirmed
implementation_evidence: observed
supersedes:
  - CAP-RPT-SCH-001
  - CAP-RPT-SCH-002
  - CAP-RPT-SCH-003
  - CAP-RPT-SCH-004
  - CAP-RPT-SCH-005
  - CAP-RPT-SCH-006
  - CAP-RPT-SCH-007
  - CAP-RPT-SCH-008
  - CAP-RPT-SCH-009
  - CAP-RPT-SCH-010
  - CAP-RPT-SCH-011
  - CAP-RPT-SCH-012
  - CAP-RPT-SCH-013
  - CAP-RPT-SCH-014
  - CAP-RPT-SCH-015
  - CAP-RPT-SCH-016
  - CAP-RPT-SCH-017
source_sha:
  services-platform: ad3f6a6582843008ed0f5a17b131878d15829af1
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
intent_sources:
  - >-
    services-platform:docs/migrations/query-scheduling-exports/README.md#D2. Schedules use the shared
    scheduling library
  - >-
    services-platform:docs/migrations/query-scheduling-exports/README.md#D3. A schedule runs a pinned copy of
    the saved query (snapshot), and says when the saved query has changed
  - >-
    services-platform:docs/migrations/query-scheduling-exports/README.md#D4. Whose access a run uses: the
    schedule's owner, checked fresh at every run
  - 'services-platform:docs/migrations/query-scheduling-exports/README.md#D5. Permissions'
  - 'services-platform:docs/migrations/query-scheduling-exports/README.md#D6. Recipients'
  - >-
    services-platform:docs/migrations/query-scheduling-exports/README.md#D7. Delivery: one e-mail per
    recipient with a secure link, through the communications module
  - >-
    services-platform:docs/migrations/query-scheduling-exports/README.md#D8. Templates: the communications
    module's templates, chosen per schedule
  - 'services-platform:docs/migrations/query-scheduling-exports/README.md#D12. Execution records and failures'
  - >-
    services-platform:docs/migrations/query-scheduling-exports/README.md#D13. Rhythm 1.0 schedules: converted
    paused, activated deliberately
  - >-
    services-platform:docs/migrations/query-scheduling-exports/README.md#Remaining differences (accepted by
    Andrew 2026-10-17)
  - 'services-platform:docs/milestones/plans/RPT-M1-plan.md#D9'
purpose: >-
  Send recurring files to designated recipients through schedules that record their execution and delivery
  outcomes.
outcomes:
  - Recurring files arrive without manual execution.
  - Automatic delivery stops when owner access is lost.
  - Query schedules retain their chosen source copy.
concepts:
  - CON-PFM-QRY-004
actors:
  - ACT-PFM-013
  - ACT-PFM-007
  - ACT-PFM-012
surfaces:
  - console
  - api
  - job
preconditions: []
postconditions: []
permissions:
  - key: platform.queries.read
    where_checked: api
    scope: tenant
  - key: platform.query-schedules.use
    where_checked: service
    scope: record
  - key: platform.query-schedules.manage
    where_checked: service
    scope: tenant
  - key: platform.query-schedules.external-recipients
    where_checked: service
    scope: tenant
  - key: platform.reports.use (report schedules)
    where_checked: service
    scope: tenant
rules:
  - id: BR-CAP-PFM-QRY-015-01
    proved_by: [ATS-PFM-QRY-015-01, TST-PFM-QRY-147, TST-PFM-QRY-349]
    statement: >-
      Schedule access requires the query surface plus schedule use or manage; users reach their own schedules,
      managers reach all, and other users receive not found.
    kind: permission
    enforced_at:
      - api
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/queries/tests/integration/services/schedules/query-schedule-routes.integration.spec.js#answers
      a colleague holding only .use 404, as for one that does not exist
    origin: 'services-platform:docs/migrations/query-scheduling-exports/README.md#D5. Permissions'
  - id: BR-CAP-PFM-QRY-015-02
    proved_by: [ATS-PFM-QRY-015-02, TST-PFM-QRY-349]
    statement: >-
      Query schedule creation requires a visible compilable source, required values, an active published
      delivery template, an hourly-or-slower timetable and active organizational user recipients.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/queries/tests/integration/services/schedules/query-schedule-routes.integration.spec.js#refuses
      a schedule more often than hourly (400)
    origin: >-
      services-platform:docs/migrations/query-scheduling-exports/README.md#D2. Schedules use the shared
      scheduling library
  - id: BR-CAP-PFM-QRY-015-03
    proved_by: [ATS-PFM-QRY-006-19, ATS-PFM-QRY-015-03, ATS-PFM-QRY-015-17, TST-PFM-QRY-349]
    statement: >-
      Query schedules run their stored source copy until explicitly refreshed, show later saved-query changes
      for comparison, and do not silently adopt those edits.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/queries/tests/integration/services/schedules/query-schedule-routes.integration.spec.js#flags
      the saved query as changed since the schedule was set up
    origin: >-
      services-platform:docs/migrations/query-scheduling-exports/README.md#D3. A schedule runs a pinned copy
      of the saved query (snapshot), and says when the saved query has changed
  - id: BR-CAP-PFM-QRY-015-04
    proved_by: [ATS-PFM-QRY-015-04, TST-PFM-QRY-350]
    statement: >-
      Automatic runs rebuild owner access and stop delivery, fail the run and pause the schedule when the
      owner cannot execute its source.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/queries/tests/integration/services/schedules/scheduled-delivery.integration.spec.js#pauses
      the schedule with the reason, on the first such run
    origin: >-
      services-platform:docs/migrations/query-scheduling-exports/README.md#D4. Whose access a run uses: the
      schedule's owner, checked fresh at every run
  - id: BR-CAP-PFM-QRY-015-05
    proved_by: [ATS-PFM-QRY-015-05, TST-PFM-QRY-140, TST-PFM-QRY-349]
    statement: 'Only schedule managers can take ownership, after which later automatic runs use their access.'
    kind: permission
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/queries/tests/integration/services/schedules/query-schedule-routes.integration.spec.js#refuses
      take-ownership to a holder of .use alone (403)
    origin: >-
      services-platform:docs/migrations/query-scheduling-exports/README.md#D4. Whose access a run uses: the
      schedule's owner, checked fresh at every run
  - id: BR-CAP-PFM-QRY-015-06
    proved_by: [ATS-PFM-QRY-015-06, TST-PFM-QRY-151, TST-PFM-QRY-349]
    statement: Run Now executes as the initiating person for the existing recipients and can run a paused schedule.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/queries/tests/integration/services/schedules/query-schedule-routes.integration.spec.js#is
      run by the consumer, as the caller, to the schedule’s recipients
    origin: >-
      services-platform:docs/migrations/query-scheduling-exports/README.md#D4. Whose access a run uses: the
      schedule's owner, checked fresh at every run
  - id: BR-CAP-PFM-QRY-015-07
    proved_by: [ATS-PFM-QRY-015-07, TST-PFM-QRY-143, TST-PFM-QRY-349, TST-PFM-QRY-350]
    statement: >-
      Schedules accept at most fifty distinct users or addresses, resolve user addresses at send time, skip
      unavailable users with reasons and require outside-recipient permission for external addresses.
    kind: permission
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/queries/tests/integration/services/schedules/query-schedule-routes.integration.spec.js#refuses
      an outside address without platform.query-schedules.external-recipients (403), and writes nothing
    origin: 'services-platform:docs/migrations/query-scheduling-exports/README.md#D6. Recipients'
    note: >-
      The recipient ceiling and uniqueness come from the contract; tests establish external-address
      authorization and skipped-user behavior.
  - id: BR-CAP-PFM-QRY-015-08
    proved_by: [ATS-PFM-QRY-014-13, ATS-PFM-QRY-015-08, ATS-PFM-QRY-015-20, TST-PFM-QRY-350]
    statement: >-
      Each recipient gets one communications request per run with a personal seven-day link, and run-recipient
      deduplication prevents duplicate e-mails on retry.
    kind: invariant
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/queries/tests/integration/services/schedules/scheduled-delivery.integration.spec.js#submits
      nothing new when the same run is run again
    origin: >-
      services-platform:docs/migrations/query-scheduling-exports/README.md#D7. Delivery: one e-mail per
      recipient with a secure link, through the communications module
  - id: BR-CAP-PFM-QRY-015-09
    proved_by: [ATS-PFM-QRY-015-09, TST-PFM-QRY-230, TST-PFM-QRY-350]
    statement: 'Query schedules default to skipping empty results, while report schedules reject that option.'
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/queries/tests/integration/services/schedules/scheduled-delivery.integration.spec.js#ends
      a skip-when-empty run skipped, its export holding no rows
    origin: >-
      services-platform:docs/migrations/query-scheduling-exports/README.md#D7. Delivery: one e-mail per
      recipient with a secure link, through the communications module
  - id: BR-CAP-PFM-QRY-015-10
    proved_by: [ATS-PFM-QRY-015-10, TST-PFM-QRY-349, TST-PFM-QRY-350]
    statement: >-
      A scheduled date value stored as today resolves to each run’s workspace day and is recorded on that
      run’s export.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/queries/tests/integration/services/schedules/scheduled-delivery.integration.spec.js#runs
      the criterion on the day of each run, in the tenant’s zone
    origin: 'services-platform:docs/queries/exports-and-schedules.md#Schedule (Andrew, 2026-10-17)'
  - id: BR-CAP-PFM-QRY-015-11
    proved_by: [ATS-PFM-QRY-015-11, TST-PFM-QRY-150, TST-PFM-QRY-350]
    statement: >-
      Transient errors retry, query and access failures do not, five consecutive failed runs pause the
      schedule with a reason, and success clears the failure count.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/queries/tests/integration/services/schedules/scheduled-delivery.integration.spec.js#pauses
      the schedule after the fifth, saying why
    origin: 'services-platform:docs/migrations/query-scheduling-exports/README.md#D12. Execution records and failures'
  - id: BR-CAP-PFM-QRY-015-12
    proved_by: [ATS-PFM-QRY-013-15, ATS-PFM-QRY-015-12, ATS-PFM-QRY-015-19, TST-PFM-QRY-230]
    statement: >-
      Report schedules require report use and visibility, render the current report as PDF by default or
      eligible Excel, flag report changes without pinning them and cannot change into query schedules.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/queries/tests/e2e/query-schedules.e2e.spec.js#refuses Excel for a dashboard
      (400 at options.format)
    origin: none
  - id: BR-CAP-PFM-QRY-015-13
    proved_by: [ATS-PFM-QRY-011-17, ATS-PFM-QRY-015-13, ATS-PFM-QRY-015-18, TST-PFM-QRY-142, TST-PFM-QRY-230]
    statement: >-
      Report deletion atomically pauses its schedules as report deleted, preserves their history and permits
      only their deletion rather than resumption or another run.
    kind: lifecycle
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: 'services-platform:modules/queries/tests/e2e/query-schedules.e2e.spec.js#refuses to resume it (400)'
    origin: 'services-platform:docs/milestones/plans/RPT-M1-plan.md#D9'
  - id: BR-CAP-PFM-QRY-015-14
    proved_by: [ATS-PFM-QRY-015-14, TST-PFM-QRY-349]
    statement: >-
      Imported legacy schedules start paused and require confirmation that the legacy schedule is disabled
      before resuming.
    kind: lifecycle
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/queries/tests/integration/services/schedules/query-schedule-routes.integration.spec.js#refuses
      a resume without confirmLegacyDisabled (400), saying why
    origin: >-
      services-platform:docs/migrations/query-scheduling-exports/README.md#D13. Rhythm 1.0 schedules:
      converted paused, activated deliberately
  - id: BR-CAP-PFM-QRY-015-15
    proved_by: [ATS-PFM-QRY-015-15, TST-PFM-QRY-139, TST-PFM-QRY-349]
    statement: >-
      Schedule edits use the read version, duplication creates a paused caller-owned copy, and deletion
      removes runs and deliveries while exported files retain their expiry.
    kind: lifecycle
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/queries/tests/integration/services/schedules/query-schedule-routes.integration.spec.js#duplicates
      it paused, named a copy, owned by the caller
    origin: none
scenarios:
  - id: SC-CAP-PFM-QRY-015-01
    given: A scheduled report’s owner loses access to its source.
    when: Automatic delivery runs and a manager later takes ownership.
    then: The failed run pauses delivery; the manager can restore responsibility for later runs.
    provenance: test
    source: 'react-console:acceptance/reporting/playwright/journeys/adversarial/RPT-ACC-0496.spec.ts#RPT-ACC-0496'
edge_cases: []
constraints: []
depends_on:
  - CAP-PFM-QRY-014
  - CAP-PFM-QRY-006
related:
  - CAP-PFM-QRY-006
  - CAP-PFM-QRY-011
  - CAP-PFM-QRY-013
  - CAP-PFM-QRY-014
journeys:
  - JNY-PFM-QRY-002
  - JNY-PFM-QRY-004
open_questions: []
operations:
  - createQuerySchedule
  - listQuerySchedules
  - getQuerySchedule
  - replaceQuerySchedule
  - deleteQuerySchedule
  - duplicateQuerySchedule
  - pauseQuerySchedule
  - resumeQuerySchedule
  - runQueryScheduleNow
  - takeQueryScheduleOwnership
  - useLatestScheduleQuery
  - getScheduleQueryChanges
  - listQueryScheduleRuns
  - previewQuerySchedule
  - listDeliveryTemplates
  - getDeliveryTemplateStarter
  - findRecipientUsers
tables:
  - query-schedules.table.ts
  - query-schedule-recipients.table.ts
  - query-schedule-deliveries.table.ts
  - schedules.table.ts
  - schedule-runs.table.ts
evidence:
  - 'Pinned reference: react-console:acceptance/reporting/playwright/journeys/adversarial/RPT-ACC-0496.spec.ts'
  - 'Pinned reference: services-platform:docs/migrations/query-scheduling-exports/README.md'
  - 'Pinned reference: services-platform:docs/milestones/plans/RPT-M1-plan.md'
  - 'Pinned reference: services-platform:docs/queries/exports-and-schedules.md'
  - 'Pinned reference: services-platform:modules/queries/tests/e2e/query-schedules.e2e.spec.js'
  - >-
    Pinned reference:
    services-platform:modules/queries/tests/integration/services/schedules/query-schedule-routes.integration.spec.js
  - >-
    Pinned reference:
    services-platform:modules/queries/tests/integration/services/schedules/scheduled-delivery.integration.spec.js
controls: [TC-AVL-004, TC-AZ-001, TC-AZ-002, TC-AZ-003, TC-AZ-004, TC-DAT-001, TC-DAT-002, TC-DAT-003, TC-DAT-004, TC-DAT-005, TC-IAM-001, TC-IAM-006, TC-LOG-001, TC-LOG-002, TC-LOG-003, TC-LOG-005, TC-MON-001, TC-PI-001, TC-PI-002, TC-PI-003, TC-PI-004, TC-PI-005, TC-PRV-001, TC-PRV-002, TC-TEN-001, TC-TEN-002, TC-TEN-003, TC-TEN-004, TC-VEN-001, TC-VEN-002, TC-VEN-003]
exercised_by: [FAS-PFM-QRY-04-03, FAS-PFM-QRY-05-01, FAS-PFM-QRY-05-02, FAS-PFM-QRY-05-03, FAS-PFM-QRY-05-04, FAS-PFM-QRY-05-05]
suites: [TST-PFM-QRY-139, TST-PFM-QRY-140, TST-PFM-QRY-142, TST-PFM-QRY-143, TST-PFM-QRY-147, TST-PFM-QRY-150, TST-PFM-QRY-151, TST-PFM-QRY-230, TST-PFM-QRY-349, TST-PFM-QRY-350]
---

# Deliver recurring query and report files

## Purpose

Send recurring files to designated recipients through schedules that record their execution and delivery outcomes.

## How It Works

A person sets up recurring delivery for a saved query or report. They choose a timetable, message template and recipients. Query schedules keep a copy of their source; report schedules render the report’s current state. Automatic work uses the schedule owner’s refreshed access. Run Now instead uses the initiating person’s access. Completed work records files and individual delivery outcomes. Retries use run and recipient identities to avoid repeated messages. Access loss or repeated failures can pause a schedule. A manager can take ownership when another person needs to become responsible for its runs.

## Relationships

Recurring delivery connects saved questions or reports with file production and the communications service.
