User Journey: Choose when a report runs

Table of Contents

This page documents a user journey: A tenant administrator gives a report definition a schedule so it runs every morning, changes that schedule, runs the report now, pauses the schedule, or takes it away, and reads what each action does and does not reach.

A user journey describes what one person is trying to do, before anything is built. It is a design document that outlives any sprint, so it names the screens, the entity fields each screen shows, and the operations and messages each step needs.

1. Goal

"Make this report run every morning." The person owns a report definition for a party and wants it produced without anybody pressing a button: at the close, at the start of the trading day, or at whatever time the desk reads it. They also want four separate answers as the routine changes. They want one run now, without waiting for the schedule. They want the schedule to stop firing for a while and to keep it. They want the same recurrence to fire again afterwards. They want the schedule gone when the report is no longer read each morning. The journey is done when the definition carries the schedule the person meant, or no schedule at all, and the person has read what the change does and does not reach.

2. Actor and trigger

The tenant administrator runs this journey. Choosing when a report runs is a tenant-level decision: it changes what the tenant's parties produce every day, and it is not a desk's choice for the afternoon. The administrator acts inside their tenant, and the definitions they can schedule are the tenant's own, party by party. A trading desk asks for the schedule; the administrator sets it.

The trigger is a decision about routine. A report that has been run by hand every morning is wanted on a schedule. A schedule that fires too early or too late is wanted at another time. A report wanted today and not at 06:00 tomorrow is wanted run now. A schedule that fires into a quiet week is wanted paused, and back on the same recurrence afterwards. A report that is no longer read each morning is wanted off the schedule. The person opens the definition from See the reports a party runs, and the journey ends when the definition's row reads the way they meant it to.

This journey writes three ways. Scheduling and unscheduling change the definition and the scheduler job that carries the recurrence. Running the report now creates a run and changes no schedule. Pausing and resuming would change neither the expression nor the job, only whether the job fires, and no operation does it today. No action here touches a run that has already happened.

3. Steps

  1. Open the definition and read its schedule. The person opens one definition and reads its schedule panel. The panel states the definition's schedule_expression, the cron expression that drives the report's recurrence, and whether the definition is scheduled at all. A definition with a scheduler_job_id is scheduled and names the job that holds its recurrence; one without it is on-demand. The panel states which of the two the person is looking at before offering to change it, because "every morning" is already true for some definitions and a person should not schedule one twice. The panel cannot state that a schedule is paused. The scheduler job carries an is_active flag and no read in this journey returns it, so a paused schedule and a live one read the same here, and What is missing records that.
  2. Write the expression. The person states the recurrence as a cron expression. The screen accepts the expression and shows it back, and it does not pretend to evaluate it: the next firing is the scheduler's to compute, and the personal reading of a cron expression is not a fact the definition stores. A person who wants "every morning" states the expression the installation's scheduler understands; the screen offers no natural-language promise it cannot keep.
  3. Press Schedule. The system asks the person to confirm, restating the definition, its party and the expression. On confirmation the screen sends reporting.v1.ops.schedule_report_definitions for the definition. The reporting service validates the expression and asks the scheduler to hold the definition's job; the definition records the job in scheduler_job_id. The service answers, and the screen reads the definition again so the panel shows the stored expression and the job, not the value the person typed.
  4. Read what the schedule now covers. The panel states that the definition is scheduled, with its expression and its scheduler job. The front door's row for this definition now reads scheduled, and the next firing creates a run there. The screen says plainly what it cannot say: it cannot show the job's next due time, the last time the schedule fired, or the scheduler's own state, because no read in this journey's set returns them. A person who wants "every morning, starting tomorrow" has to trust the expression and the scheduler, and What is missing records that.
  5. Change the time. The person edits the expression and presses Schedule again. The same operation is sent with the new expression and the service reconciles the definition's schedule. The screen does not simulate the change: it sends it, reads the definition back, and states the result.
  6. Run now. A person who wants one run without waiting for the next firing presses Run now. The system asks the person to confirm, and the confirmation states what the action reaches: it starts one run at once, and it does not change the schedule, the expression or the scheduler job. On confirmation the screen sends reporting.v1.ops.trigger_report_instance for the definition. The reporting service applies the definition's concurrency policy, creates a report_instance, and dispatches the execution workflow. The screen does not wait for the run: it states that the run started and hands the person to Run a report and watch it finish. Run now serves an on-demand definition as well as a scheduled one, because running when a person asks is what an on-demand definition is for. The scheduler sends the same operation when a job fires, so a build that offers it to a person must name the permission that gates a person's call.
  7. Pause. A person who wants the schedule to stop firing, and to keep it, presses Pause. The system asks the person to confirm, and the confirmation states what the action does and does not reach: the recurrence stays on the definition, the scheduler job stays, and nothing fires until the person resumes; the next fire time is unknown while the schedule is paused, and the screen says so instead of drawing one. No operation does this today. The scheduler job carries is_active in ores_scheduler_job_definitions_tbl, so the data can express a paused schedule, and nothing exposes it to a reporting screen. This journey names the action because a person asks for it, and records the operation as missing rather than inventing a subject for it.
  8. Play. A person whose schedule is paused presses Play. The system asks the person to confirm, and the confirmation states that firing starts again on the same schedule: same expression, same job, and the next time the expression names. Nothing is re-entered and no run is created by the action itself. No operation does this today, for the same reason: the state would live in the job's is_active and nothing writes it from a reporting screen.
  9. Unschedule. A report that is no longer wanted on a schedule is unscheduled with reporting.v1.ops.unschedule_report_definitions. The system confirms, because taking a schedule away changes what the tenant produces every day. On confirmation the service removes the definition's schedule, and the definition no longer names a scheduler job. The definition stays, and it becomes on-demand: a person who wants the report can still run it now. Unscheduling is not pausing. It removes the expression and the job with it, so there is no recurrence left to resume.
  10. Read what these actions do not do. The runs already produced stay, with their outputs and their errors. No action here deletes history, and none stops a run that is in flight: there is no operation in this journey that cancels a run. Run now does not change the schedule, and pausing does not remove a run. The screen says all of it, so a person who expected "unschedule" to mean "stop everything" is not left believing it did.
  11. Leave, or go on. The person returns to the front door and checks the row, or opens the definition's latest run to see what the schedule last produced. The run the schedule produces is See the reports a party runs's row and Run a report and watch it finish's page; this journey changes when that happens and nothing else.

4. Screens and wireframes

One screen. The definition header, the schedule panel, the expression editor and the actions are panels of the same screen, because the person reads them as one answer. Each action carries its own confirmation, a dialog that restates the definition, the party and the expression, because the change outlives the person's session. The confirmation also says what the action does not reach, so Run now, Pause and Unschedule cannot be read as one another.

The screen writes three ways at most: it schedules, it unschedules, and it starts one run. Every other message on it is a read. The panel distinguishes "scheduled" from "on-demand" by the presence of the definition's scheduler job. It cannot distinguish a paused schedule from a live one, because the job's is_active is unread here. It says what it cannot show rather than drawing a next-run time it did not read.

wireframe_choose_when_a_report_runs.png

Figure 1: Choose when a report runs: the definition, its current expression, the schedule editor, and what the schedule does and does not cover.

5. Prototype

The prototype argues this journey in three interpretations, and its design note says what each argues and what is fake. This journey is the design intent for the schedule panel, and its prototype belongs at doc/prototypes/reporting/choose-when-a-report-runs/. The prototype must draw a definition that is on-demand with no scheduler job; the confirmation that sets a schedule; the same definition scheduled, with its expression and its job; the change of an expression; the confirmation that runs the definition now; the confirmation that pauses a schedule, and the same definition resumed; the confirmation that unschedules; and the definition back on-demand with its past runs still listed. It must draw Run now, Pause or Play, and Unschedule as three separate actions, and it must show a paused definition as paused while its expression stays readable. Pause and Play have no operation, so the prototype draws them as a fixture and says so. It must also draw the panel's honest statement that the next firing and the last firing are not readable, so the prototype does not imply a next-run time.

6. Entities composed

Entity What it contributes Model
report_definition The definition whose recurrence changes: schedule_expression, scheduler_job_id, concurrency_policy and the party that owns it ores.reporting.report_definition
report_instance The runs the schedule has already produced, which unscheduling leaves in place ores.reporting.report_instance
party The owner of the definition whose routine changes ores.refdata.party

The definition is the row this journey edits, and scheduler_job_id is how the screen knows the definition has a schedule. The runs are the definition's history, and they are not part of the schedule: unscheduling does not remove them. The firing a schedule produces is a report_trigger_event, immutable and written once; it is named here because it is what the screen would read to answer "did the schedule fire", and no read serves it.

The paused state does not live on the definition. It lives in the scheduler job: ores_scheduler_job_definitions_tbl carries is_active, and a paused schedule would be a job whose is_active is false. The definition stores only the job's identifier, so a screen that reads the definition alone reads a paused schedule and a live one the same. The table is named here because it is where the state belongs, and no operation in this journey reads or writes that column.

Every entity above carries party_id and is party-scoped by row-level security. The definition and the run are bitemporal, so a schedule change is a new version of the definition and the earlier version stays readable. The trigger event is an immutable event.

7. Operations and messages

Step Operation Subject Status
Open the definition Read the definition and its expression reporting.v1.report_definitions.get exists
Open the definition List the party's definitions reporting.v1.report_definitions.list exists
Schedule Give the definition a schedule reporting.v1.ops.schedule_report_definitions exists
Schedule Read the definition back after the write reporting.v1.report_definitions.get exists
Change the time Give the definition a new schedule reporting.v1.ops.schedule_report_definitions exists
Run now Start one run immediately, without touching the schedule reporting.v1.ops.trigger_report_instance exists
Run now Follow the run the trigger created reporting.v1.report_instances.list exists
Pause Stop the schedule firing and keep it (no pause operation; the state would be the job's is_active) missing
Play Start firing again on the same schedule (no play operation; the state would be the job's is_active) missing
Unschedule Take the schedule away reporting.v1.ops.unschedule_report_definitions exists
Unschedule Read the definition back after the write reporting.v1.report_definitions.get exists
Read the past runs List the runs the schedule already produced reporting.v1.report_instances.list exists
Read the firing Read when the schedule last fired, and what it triggered (no read of report_trigger_event over NATS) missing
Read the scheduler job Read the job's state or its next due time (no scheduler read is named in this set) missing
The screen itself Read and change a schedule from the browser (no BFF route) missing

Three operations are this journey's writes, and run now is one of them. The two schedule operations are served by the reporting service, which asks the scheduler to hold one job per definition. trigger_report_instance is served by the same service: it applies the definition's concurrency policy, creates a report_instance, and dispatches the execution workflow. The scheduler sends that same operation when a job fires, so its caller is normally a service and not a person. The definition read returns schedule_expression and scheduler_job_id, so the screen can state whether a definition is scheduled and what expression it carries. It cannot state the job's own state or its next due time, because those belong to the scheduler and no operation here reads them.

The permission that gates these operations is not uniform. The scheduling handler checks a permission before it schedules or unschedules, so those two writes are gated. The run-now handler checks none. It serves reporting.v1.ops.trigger_report_instance and contains no permission test anywhere, while its own documentation states that the caller's permission check is what decides whether it may act in a tenant. Any authenticated caller can therefore trigger a run today. The definition read is gated by reporting::report_definitions:read. The tenant administrator holds * and reaches all three. A role below the administrator has no stated code to be granted for the schedule writes, and needs none at all for run now, which What is missing records.

No BFF route serves this screen. The prototype adds one, for example GET /api/reporting/reports/<definition-id>/schedule for the read, PUT /api/reporting/reports/<definition-id>/schedule for the schedule and unschedule writes, and POST /api/reporting/reports/<definition-id>/runs for run now, which calls trigger_report_instance. The handler authenticates the caller's token and checks the definition read and run write codes.

8. What is missing

  1. A route and a screen. No BFF route serves reporting, and nothing in the web application shows a report today. The schedule panel, its editor and its confirmations are new.
  2. The firing is stored and unreadable. report_trigger_event records every firing with its definition, its trigger type, its scheduler job and its time. There is no read of it over NATS. The screen can say a definition is scheduled, and it cannot say when the schedule last fired, whether it fired at all, or which run a firing produced. That is the central gap for a person who has set a schedule and is waiting for the first result.
  3. No read of the scheduler job. The definition stores scheduler_job_id, and no operation in this set reads the job behind it. The screen cannot show the job's state, its next due time or the scheduler's own view of the expression, so "every morning" is a promise the screen can only repeat, not verify.
  4. No time zone. report_definition states a cron expression and no time zone. A tenant whose morning is not the installation's morning cannot state which morning it means, and the screen cannot say whose clock the expression is read on. A time zone on the definition, or a stated installation default, is the missing field.
  5. The write permission is not named, and run now is not gated at all. The two schedule operations have no permission code in this journey's set. TenantAdmin and SuperAdmin hold * and can call them; no role below the administrator can be granted the capability because there is no code to grant. A write code for the definition is the missing piece. Run now is worse than unnamed: its handler performs no permission test, so any authenticated caller can start a run. The handler's own documentation claims the caller's permission check decides, and no such check exists.
  6. No pause and no play. The scheduler's job row carries is_active in ores_scheduler_job_definitions_tbl, so the data can express a paused schedule, and nothing exposes it to a reporting screen. reporting.v1.ops.schedule_report_definitions and reporting.v1.ops.unschedule_report_definitions are the only two schedule operations, and neither sets is_active. A pause operation and a play operation are the missing pair, and a read of the flag is the missing read: without it the screen cannot tell a paused schedule from a live one, and Pause cannot be offered honestly. No subject is invented for them here.
  7. Unscheduling is not pausing, and neither stops a run. Unschedule removes the expression and the scheduler job, so the definition has no recurrence left. Pause would keep both and stop only the firing. Neither action cancels a run in flight, and neither removes past runs or their archives. The screen states all of it. A person who needs a running report stopped has no action in this journey.
  8. No preview and no validation on the screen. The expression is validated by the service when the write arrives, and the screen does not evaluate it. A bad expression is refused by the service, and the screen shows the refusal. A next-firing preview would need the scheduler's own evaluation, which no read here provides.

9. Related journeys

10. Related knowledge

11. See also

Emacs 29.3 (Org mode 9.6.15)