Cron jobs
Area: operations. Cross-cutting background workers, not a single module. They must be scheduled on the server in the system crontab. They live in the top-level cron/ directory as standalone PHP scripts run by the OS cron daemon, never from the app UI.
How this page was verified
The directory listing was read on the production branch of each of the big five on 2026-09-16. The Multiple Sign Off job was read on master at the INA01-866 commits e230a25be and c63dd6658, which were still local and not yet on origin/main when this page was written. The other jobs were read on master on 2026-09-06.
Several jobs call an in-app HTTP endpoint with cURL, so the app base URL ($inweb_baseurl, the baseurl row of inweb_config) must be reachable from the server itself and TLS must work. These calls disable SSL_VERIFYPEER.
Minimal production crontab
The master README.md installs the first two entries. The Multiple Sign Off job is needed wherever users sign off several routings at once. The last two are the config-gated notification jobs.
# every minute — flush the outgoing email queue (SMTP via PHPMailer)
* * * * * php /path/to/app/cron/inact_mailgun_curl.php >> /dev/null 2>&1
# every minute — procurement submission queue
* * * * * php /path/to/app/cron/cron_submit_queue.php >> /dev/null 2>&1
# every minute — finish Multiple Sign Off (approved document, transmittals, library publish)
* * * * * php /path/to/app/cron/cron_multiple_signoff_v2.php >> /var/log/inact_multisignoff.log 2>&1
# hourly — routing digest (only acts when enable_routing_notif_digest=1)
0 * * * * php /path/to/app/cron/inact_routing_digest.php
# daily — overdue / upcoming routing notifications
0 7 * * * php /path/to/app/cron/cron_overdue_mail.phpAdjust the PHP binary and path per environment and redirect output to a log so failures are visible. Enable the retention and digest crons only if their feature flags are on. No crontab line is installed by a deploy: the master deploy/ scripts and .github/workflows/deploy.yml do not touch cron. Every line above is a manual step on every server. Production crontabs are the client's, see each instance page.
The jobs
1. inact_mailgun_curl.php calls inact_mailgun2.php: outgoing email queue, every minute
All outgoing email is queued into ts_email; nothing is sent synchronously. The wrapper calls inact_mailgun2.php, which selects pending rows (ts_email WHERE email_sent='n', oldest first, batch size from EMAIL_TOTAL_PER_BATCH), marks them 'q', and sends each via SMTP (PHPMailer) using the $inweb_smtp_* settings. On success email_sent='y' plus a sent timestamp; on failure 'e'. Respects a daily quota (EMAIL_QUOTA_PER_DAY, -1 = unlimited). Despite the "mailgun" name, delivery is SMTP.
- Tables:
ts_email(email_sent:n, thenq, thenyore). - Config:
.envkeysEMAIL_QUOTA_PER_DAY(default-1) andEMAIL_TOTAL_PER_BATCH(default10), read with$_ENV[...] ?? defaultininact_mailgun2.php. Neither is in the example env file; set them only to change the defaults. - If not running: no email is ever sent. Every notification piles up unsent.
The wrapper targets inact_mailgun2.php in all five forks.
2. cron_multiple_signoff_v2.php: finish Multiple Sign Off, every minute
Multiple Sign Off records every sign off at once, then queues the slow work in ts_multiple_signoff_list, one row per signed-off performer (rout_id, map_id). This job does that slow work. It is the master version since INA01-866; the old cron_multiple_signoff.php, cron_create_routing_slip.php and getPDFMultipleSignoffCron.php are gone from master.
| The performer who signed off | What the job runs | Result |
|---|---|---|
| Inside the Document Controller part of the routing (D - Transmit) | publishTransmitSignOff() | Document Library record, outgoing Transmittal when the issued status is external and enable_transmittal = 1, library folder. One DB transaction. |
| Anyone else (A - Approval, a rejecting C / S, the last I - Information) | finishApprovalSignOff() | curl to getPDFAfterApprove.php (the _R document), return Transmittal when external and enable_transmittal = 1, library folder after an approved sign off |
- Batch: 10 rows, oldest
multi_idfirst, onlymulti_status = 'n',cron_ongoing'n'or NULL, andmap_idnot NULL. - Claim: each row is set to
cron_ongoing = 'y'before its work starts. Success setsmulti_status = 'y',cron_ongoing = 'n'. Any exception or PHPErrorsets both back to'n', so the row is retried next minute. There is no retry limit. - Each row is claimed on its own, with one conditional UPDATE that only matches a row still free. Two runs that overlap never take the same row, so no lock file or
flockis needed. A skipped row printsqueue row 12: taken by another run, skipped. - Each row runs as the performer who signed off (
getSignOffPerformer()), so the document author and logs show that person, not a system user. - Flags: the publish needs
enable_autopublish = 1; the_Rdocument for A needs it too. I - Information always builds its document. - If not running: sign offs are recorded and routings close, but no approved
_Rdocument, no Transmittal and no Document Library record appear. Rows pile up atmulti_status = 'n'. - Detail: Multiple Sign Off.
3. inact_routing_digest.php: routing digest, hourly, flag-gated
When enable_routing_notif_digest='1', routing_notif_digest_hour is not empty, and the current hour is in that CSV, it groups outstanding ts_map_routing_to rows by project and recipient and sends one consolidated digest email per user, queued via ts_email.
- Config:
enable_routing_notif_digest,routing_notif_digest_hour, see inweb_config. - If the flag is off: exits immediately.
- Present in: master, Jadestone, Medco. Not in JOTRE or Prima Energy.
4. inact_routing_overdue_digest.php: overdue digest, flag-gated
Reads enable_routing_overdue_notif_digest straight from inweb_config and, when on, sends the overdue-items digest. Present in master and Jadestone only; the key exists only in their seeds.
5. cron_overdue_mail.php: overdue and upcoming notifications, daily
Sends per-recipient "overdue" and "due soon" notices for routings near or past rout_due_date, honouring working days and holidays. The look-ahead window is the .env key NOTIF_OVERDUE_SINCE_DAY (1 = warn one day ahead, the default when empty; 0 = today; negative = past due). Present in all five forks.
Set up the Multiple Sign Off cron
Do this on every server that runs master after INA01-866: dev, staging and production. A deploy does not do it for you.
1. Check the prerequisites
| Check | Why | How to check |
|---|---|---|
The migration 20260915090000_alter_ts_multiple_signoff_list_add_map_id has run | The job reads ts_multiple_signoff_list.map_id | Dev and staging deploys run phinx migrate on their own. Production needs a manual run: php vendor/bin/phinx migrate |
| PHP CLI, not PHP-FPM | The script exits at once, with no output, under any other SAPI (PHP_SAPI !== 'cli') | php -v |
| PHP CLI loads the same extensions as the web PHP | setting.php loads Composer classes, creates a MongoDB client and needs curl | php -m and compare with the web phpinfo() |
The app .env is in the app root | setting.php loads .env from the app root. The script changes to that directory itself, so the crontab needs no cd | ls /path/to/app/.env |
baseurl in inweb_config is reachable from the server itself | finishApprovalSignOff() calls <baseurl>/includes/javascripts/tracking/pdfviewer/web/api/getPDFAfterApprove.php, timeout 600 s, SSL verify off | See the curl command below |
| MongoDB is reachable | Every logged write ($pdo->log()) inserts into the inweb_logs collection. A failed insert is caught and sent to the PHP error log, so it does not stop the job | MONGO_* keys in .env |
| The cron user can write the upload folders | Transmittal PDFs are written under docUpload/transmittal | Run the job once as that user (step 3) |
Check that the base URL answers from the server. First read it from the database:
SELECT config_value FROM inweb_config WHERE config_name = 'baseurl';Then call the page from the server, with that value in place of <baseurl>:
curl -k -s -o /dev/null -w '%{http_code}\n' "<baseurl>/includes/javascripts/tracking/pdfviewer/web/api/getPDFAfterApprove.php"Any HTTP code below 400 is fine. The job treats a curl error or an HTTP code of 400 or more as a failure.
2. Install the line and remove the old one
On Linux and macOS, add the line with crontab -e as the user that owns the app files:
* * * * * php /path/to/app/cron/cron_multiple_signoff_v2.php >> /var/log/inact_multisignoff.log 2>&1No lock file and no flock. Each queue row is claimed with one conditional UPDATE, so two runs that overlap never take the same row. That is also why the line works the same on Windows.
On Windows (Tomori runs on IIS) there is no crontab. Make a Task Scheduler task that repeats every minute and runs the same command. Tomori's paths, from its instance page: PHP is C:\php\php.exe and the app sits in C:\inetpub\wwwroot\inact, so the program is C:\php\php.exe and the argument is C:\inetpub\wwwroot\inact\cron\cron_multiple_signoff_v2.php.
TODO: the exact Task Scheduler settings for this job on the Windows servers — task name, the account it runs as, and where its output is written — are not documented anywhere yet.
Remove any cron_multiple_signoff.php line (the v1 job). Its file no longer exists in master, so the line only fills the cron mail or log with errors. Make sure the log file is writable by that user.
3. Run it once by hand
php /path/to/app/cron/cron_multiple_signoff_v2.php4. Check that it runs
Watch the log:
tail -f /var/log/inact_multisignoff.logWhat the job prints, taken from the script:
| Line | Meaning |
|---|---|
multiple sign off queue is empty | Healthy, nothing to do. Printed once a minute when idle |
multiple sign off queue: 3 row(s) | A batch was read |
queue row 12: taken by another run, skipped | Another run already claimed that row. Harmless |
routing 120 (map 845): publish then published as doc no 9031 | D - Transmit row. nothing to publish means enable_autopublish is off or the routing has no document file |
routing 121 (map 850): approved document | A / C / S / I row, curl to getPDFAfterApprove.php |
done | The row is finished (multi_status = 'y') |
WARNING: return transmittal failed: ... | The return Transmittal failed, but the row is still marked done, as a single Sign Off does |
ERROR: ... | The row went back to 'n' and runs again next minute |
done | End of the batch |
ERROR: cannot read ts_multiple_signoff_list: ..., or an uncaught PDOException | The queue query failed. Usually the migration has not run, so map_id does not exist |
Check the queue in the database (portable SQL):
SELECT multi_id, rout_id, map_id, multi_status, cron_ongoing FROM ts_multiple_signoff_list WHERE multi_status = 'n' ORDER BY multi_id;Healthy: rows with map_id set leave this list within a minute or two of a Multiple Sign Off.
5. Clear a stuck claim
A run killed part way (a PHP fatal error, out of memory, a server restart) leaves its rows at cron_ongoing = 'y', and later runs skip them. First make sure no run is active (pgrep -f cron_multiple_signoff_v2.php prints nothing), then:
UPDATE ts_multiple_signoff_list SET cron_ongoing = 'n' WHERE multi_status = 'n' AND cron_ongoing = 'y';Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Log shows ERROR: getPDFAfterApprove failed: ... every minute, rows stay at n | baseurl is not reachable from the server (DNS, firewall, a public URL that does not loop back), or the page returns HTTP 400 or more | Run the curl check in step 1. Fix DNS / hosts or inweb_config.baseurl |
| Nothing in the log at all | The line is not installed, the log path is not writable, or PHP is not the CLI binary | crontab -l, check the log file owner, php -v |
Rows stay at n with cron_ongoing = 'y' | A run was killed part way | Step 5 |
Rows stay at n with map_id NULL | Old rows written before INA01-866. The job skips them by design | Check each routing by hand, then set multi_status = 'y' |
ERROR: cannot read ts_multiple_signoff_list or an uncaught PDOException about map_id | Migration not run | Run phinx migrate. Until then every Multiple Sign Off in the browser also fails with "Sign off failed" |
ERROR: performer row N not found | The performer row was deleted after the sign off | Check the routing, then set the row to multi_status = 'y' |
The queue drains but A routings get no _R document | enable_autopublish is off. The job then builds only the return Transmittal (when external and enable_transmittal = 1) | Expected. Turn the flag on only if the instance should publish |
| A run takes longer than a minute and the next one starts | Normal. Each row is claimed on its own, so the second run only picks up rows the first has not taken | Nothing to do |
TODO: after a curl timeout (600 s) the row is retried, but the web request may still finish on the server. Whether a retry can build the
_Rdocument twice has not been checked.
TODO: a failed D - Transmit publish rolls back the database, but a Transmittal PDF already written to
docUpload/transmittalstays on disk. Whether that file is cleaned up anywhere has not been checked.
The rest of cron/
Master holds 18 files. Beyond the five above:
| Script | Purpose | Gated by | Present in |
|---|---|---|---|
inact_doclib_retention.php | Flag and notify documents approaching their retention or expiry | enable_doclib_retention | master, Jadestone, Medco, JOTRE |
inact_direct_update_data.php | Super-admin maintenance, fixes stale routing inbox markers | super-admin only | all five |
cron_bypass_overdue.php | Overdue-bypass handling | — | all five |
cron_create_routing_slip.php | Generate routing slips for the old multiple sign-off cron | — | Jadestone, Medco, JOTRE, Prima Energy. Removed from master by INA01-866 |
cron_issue_staging_oversize.php | Email about staged document file sizes. Not an oversize-page converter. | — | all five |
cron_submit_queue.php, procurement_submit_attachment.php, pl_submit_attachment.php | Procurement and packing-list submission and attachment queues | — | all five |
expediting_weekly_cron.php | Weekly expediting job (procurement) | — | all five |
cron_dump_db.php | Scheduled database dump | — | all five |
cron_delete_empty_emailto.php | Email cleanup | — | all five |
cron_pool_idapd.php | Sends an email; purpose not documented in the file | — | all five |
setting.php | Bootstrap shared by the scripts, not a job | — | all five |
TODO: what
cron_pool_idapd.phpis for.
Operational notes and gotchas
- Email is fully asynchronous. Everything funnels through
ts_emailand job 1. The digest and overdue jobs also queue intots_email, so job 1 stays critical. - cURL dependency. Job 1 and the A / C / S / I half of job 2 call back into the app over HTTP. A wrong or unreachable
$inweb_baseurlmakes job 1 do nothing silently, and makes job 2 retry the same rows every minute. - Flag-gated crons do nothing silently when off.
inact_routing_digest.php,inact_routing_overdue_digest.phpandinact_doclib_retention.phpexit early unless their feature flags are set. Easy to mistake for "broken". - The idle Multiple Sign Off job still writes one log line a minute. Rotate
/var/log/inact_multisignoff.log.
Instance differences
Checked on 2026-09-16 on each fork's origin/main. Master is counted after INA01-866.
| Instance | Multiple Sign Off cron | Other differences |
|---|---|---|
| Master (after INA01-866) | cron_multiple_signoff_v2.php only | 18 files |
| Jadestone | both cron_multiple_signoff.php and cron_multiple_signoff_v2.php, plus cron_create_routing_slip.php. Its v2 is its own version, not the master file | 20 files |
| Medco | cron_multiple_signoff_v2.php (its own version), cron_create_routing_slip.php and cron_create_routing_slip_old.php | 21 files. cron_daily_summary.php still present, plus inact_archive_mail.php. No inact_routing_overdue_digest.php. |
| JOTRE | cron_multiple_signoff.php (v1) and cron_create_routing_slip.php | 32 files. Adds twelve accurate_* scripts (sync and backfill against the Accurate accounting system) and backfill_expediting_chain.php, backfill_mrr_no.php. No digest crons. |
| Prima Energy | cron_multiple_signoff.php (v1) and cron_create_routing_slip.php | 16 files. No inact_doclib_retention.php, no digest crons. |
TODO: which file Jadestone's servers schedule, v1 or v2, when both are present.
Related
- Multiple Sign Off fills the
ts_multiple_signoff_listqueue job 2 drains. - Sign Off: the single sign off the job copies.
- inweb_config lists the flags gating the digest and retention crons.