# The Purge Job: one schedule for all cleanup

<div style="text-align: justify;">

An instance that renders all day accumulates. History records with their PDFs, print tasks that have long been printed, mail tasks whose mails arrived weeks ago, and files on the history disk that no record points at any more. Somebody has to take that out, on a clock, without anybody thinking about it.

The **Purge Job** is that somebody. One schedule, one run, five entities, five retentions.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>scheduler.purge-form.png</code><br>The purge settings with one retention field per entity and the switch that turns a step off.
</div>

---

<h3 id="steps" style="color: #203671; margin-top: 2.2em;">The five steps</h3>

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Step</th>
      <th style="text-align: left; padding: 6px 10px;">Deletes</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">1. Print Tasks</td>
      <td style="padding: 6px 10px;">Print tasks past the retention that have reached status <code>Printed</code>. One that never printed stays, so a problem does not disappear before anybody looked at it.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">2. Mail Tasks</td>
      <td style="padding: 6px 10px;">Mail tasks past the retention, whatever their status. The mail itself is long gone, the record is the receipt.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">3. History Records</td>
      <td style="padding: 6px 10px;">History records past the retention, together with their PDF and their xlsx export.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">4. Orphaned Files</td>
      <td style="padding: 6px 10px;">Files on the history disk that no history record and no mail task refers to any more, and that are older than the retention.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;">5. Scheduler Run Log</td>
      <td style="padding: 6px 10px;">The run records of the Job Scheduler itself. The run doing the deleting never deletes itself.</td>
    </tr>
  </tbody>
</table>

<div style="border-left: 4px solid #F9C846; background: #fffbf0; padding: 12px 16px; margin: 16px 0; color: #525E5A;">
<strong>The order is fixed and not configurable.</strong> Print tasks and mail tasks point at the history records they belong to, so the database refuses to delete a record while one of them still references it. Running the steps in this order is what makes the cleanup work, and it is the reason all five belong in one schedule instead of five.
</div>

---

<h3 id="retention" style="color: #203671; margin-top: 2.2em;">Saying how long</h3>

Every step has its own retention in days, and every step can be switched off on its own.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Value</th>
      <th style="text-align: left; padding: 6px 10px;">Means</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>90</code></td>
      <td style="padding: 6px 10px;">Delete what is older than 90 days.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>0</code></td>
      <td style="padding: 6px 10px;">Delete everything older than right now. Useful for a one-off clear-out, dangerous as a standing setting.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;">switched off</td>
      <td style="padding: 6px 10px;">That entity is never purged. The step is named under <code>skipped</code> in the run result, so the log shows it was a decision and not an accident.</td>
    </tr>
  </tbody>
</table>

A sensible starting point keeps print and mail tasks shorter than the history records they belong to, and the orphaned files shortest of all. Something like 30, 30, 90, 7 and 90 days.

---

<h3 id="when" style="color: #203671; margin-top: 2.2em;">Saying when</h3>

A purge schedule says when it runs the same way every other schedule does: with a cron expression for the nightly housekeeping, or with a single date and time for a cleanup that belongs to one occasion, such as the clear-out after a migration. Both are on the *Job Scheduler* page.

A purge schedule that is waiting for its single date counts as active cleanup for as long as that date is still ahead, so the overview does not warn about missing housekeeping while one is armed. Once it has run, that cleanup is over; a *0* retention that was meant for exactly that one run does not stay armed for the next night.

---

<h3 id="setup" style="color: #203671; margin-top: 2.2em;">Where it comes from</h3>

The base setup of an instance creates the schedule under the name **Purge Job**, deactivated and without a cron expression. Its five retentions start from the `PURGE_*_DAYS` keys of the `.env` file, which are read once at that moment. From then on the retentions live on the schedule, and the `.env` keys are not read again.

Nothing is cleaned up until you give the schedule an expression and switch it on. That is deliberate: a fresh instance should not start deleting on a schedule nobody chose.

```
0 3 * * *     every night at 03:00
0 3 * * 0     Sunday nights only, for an instance with little traffic
```

---

<h3 id="permissions" style="color: #203671; margin-top: 2.2em;">Who may switch a step on</h3>

A Purge Job has no owner. It runs without any user at all, which means the usual permission check has nothing to check against. So the check moves to the moment a step is configured: whoever switches a step on, or changes its retention, has to hold the permission to delete what that step deletes.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Step</th>
      <th style="text-align: left; padding: 6px 10px;">Needs</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Print Tasks</td>
      <td style="padding: 6px 10px;"><code>report-print-task:delete</code></td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Mail Tasks</td>
      <td style="padding: 6px 10px;"><code>report-mail-task:delete</code></td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">History Records, Orphaned Files</td>
      <td style="padding: 6px 10px;"><code>report-history-record:delete</code></td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;">Scheduler Run Log</td>
      <td style="padding: 6px 10px;"><code>scheduled-job:delete</code></td>
    </tr>
  </tbody>
</table>

Steps you leave untouched are not asked about again, so somebody may change the cron expression without holding every delete permission in the instance.

---

<h3 id="result" style="color: #203671; margin-top: 2.2em;">What a run tells you</h3>

The run record carries the numbers per step: how many records were eligible, how many were deleted, how many could not be. A step that throws does not stop the ones after it.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Outcome</th>
      <th style="text-align: left; padding: 6px 10px;">Status</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Every configured step finished</td>
      <td style="padding: 6px 10px;"><code>Success</code></td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Some steps finished, one failed or left records behind</td>
      <td style="padding: 6px 10px;"><code>Warning</code>, with the reason in the result</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px;">Every configured step failed</td>
      <td style="padding: 6px 10px;"><code>Error</code>, and the failure mail goes out</td>
    </tr>
  </tbody>
</table>

A history record that could not be deleted because something still references it is logged individually, not swallowed into a count.

---

<h3 id="commands" style="color: #203671; margin-top: 2.2em;">Doing it by hand</h3>

The first four steps are each available as an artisan command as well, for a one-off cleanup or a maintenance window.

```bash
php artisan report-print-tasks:purge --days=30
php artisan report-mail-tasks:purge --days=30
php artisan report-history-records:purge --days=90
php artisan orphaned-files:purge --days=7
```

Run them in that order for the same reason the schedule does.

---

<h3 id="deleted" style="color: #203671; margin-top: 2.2em;">If the schedule is gone</h3>

The Purge Job is an ordinary schedule and can be deleted like any other. Nothing then cleans up, and the disk keeps filling. The Job Scheduler overview shows a warning for as long as no active purge schedule exists.

There are two ways back, and neither of them is special.

**From the frontend.** **New** on the Job Scheduler overview offers **Purge Job** next to Render Job. Pick it, set the retentions, give it a cron expression and switch it on. A purge schedule is created like any other schedule; it needs no owner and no API token, only a name that is still free and the delete permissions of the steps you switch on. The same way you would create a second one, for instance a weekly run with longer retentions next to a nightly one.

**From the command line**, for an instance you would rather not click through:

```bash
php artisan scheduler:create-purge-job
```

It creates the schedule under its original name when it is missing and does nothing when it is already there. It touches neither storage nor users, so it is safe on a live system. Afterwards give it its cron expression and switch it on again.

</div>