Scheduler
Overview
The Scheduler is the tenant’s cron table. Each row pairs a background job processor with a cron expression and, optionally, a JSON payload of properties for that job. On every pacemaker tick the platform reads the tenant’s active rows, works out which are due, and puts the matching job on the tenant queue.
It is how recurring work gets scheduled without a deployment: e-invoice submission sweeps, replenishment runs, report generation, notification pushes and the rest. It runs nothing itself and knows nothing about what the jobs do.
Where it fits
| Upstream | This applet | Downstream |
|---|---|---|
The tenant’s registered job processors (bl_job_processor_hdr — queue code, name, type, status) | A row in bl_sch_crontab_hdr: code, name, cron expression, event properties, status | The tenant queue, via MainTenantProcessor |
| — | Each firing | A bl_sch_crontab_event row (the run history) and an updated last_run_time |
| — | The Run button | An app_scheduler_queue row processed immediately, outside the cron path |
Screens and menus
| Menu | Route | What it lists | Actions |
|---|---|---|---|
| Scheduler | scheduler | bl_sch_crontab_hdr — Scheduler Code, Scheduler Name, Status, Last Run Date, Created Date, Updated Date | Add, open |
| Settings → Field Settings, Default Selection | settings/field-settings, settings/default-selection | See Configuration | — |
| Settings → Webhook, Feature Visibility, Permission Set / User / Team / Role | settings/… | The shared screens | — |
| Personalization → Default Selection, Sidebar | personalization/… | Per-user | — |
Create has two tabs. Details holds Scheduler Code (read-only — clicking it opens a Select Processor picker over the tenant’s job processors, showing Code, Name, Type and Status), Scheduler Name, and Status (ACTIVE / INACTIVE). Cron Expression embeds the ngx-cron-editor widget in standard flavour, pre-set to 0 0 1/1 * *. CREATE is disabled until code, name and status are filled.
Edit adds a third tab, Json, for the event properties — a JSON editor pre-filled from the selected processor’s own property_json when the row is new, or from the row’s saved event_properties, and able to load a .json file. Edit also shows Created Date and Modified Date, and carries Run and Delete.
The Personalization menu lists a Field Settings item pointing at field-settings, which is not a child route of personalization — selecting it falls through to the applet’s 404 (menu-items.ts vs app.routing.ts, commit 3fd3986).
Screenshots needed
No screenshots exist for this applet. A capture session should take: the Scheduler listing with several rows and their Last Run Date; the Select Processor picker; the Cron Expression tab with the editor open; and the Edit screen’s Json tab with a payload and the Run button. Job processor codes are product-wide, not customer data, so a demo tenant is enough.
Configuration
Before you can use it
| Prerequisite | Where | Why |
|---|---|---|
| Job processors registered and enabled for the tenant | Platform / BigLedger support; visible in the Developer SysAdmin Applet | The Select Processor picker lists bl_job_processor_hdr. A row whose code is not a registered processor cannot be created from the UI and would be skipped at run time anyway. |
| The pacemaker running for the tenant | Platform | MainTenantProcessor is what reads the cron table. Nothing in this applet polls. |
API_TNT_DM_SCH_CRONTAB_* permissions | Settings → Permission Set / Role Permission | See below. |
Applet settings
Settings are applet-local, and neither screen does anything for this applet.
- Settings → Field Settings is the unbound eight-toggle stub (Unit Discount, SST/VAT/GST, WHT, Blanket Order, Segment, G/L Dimension, Profit Center, Project) — no form binding, SAVE has no handler, and the labels belong to a sales-document screen. No exposed control found (routes and settings components checked at commit
3fd3986). - Settings → Default Selection and Personalization → Default Selection offer Default Branch and Default Location. Neither key is read anywhere in the applet.
Everything that matters is on the schedule row itself:
| Setting | Where | What it controls | Default |
|---|---|---|---|
| Scheduler Code | Create → Details → Select Processor | Which job runs. Stored upper-cased and trimmed; it must match a JobProcessorClassName enum constant or the row is silently ignored by the trigger. | none — required |
| Cron Expression | Create / Edit → Cron Expression | When it is due. Parsed with the UNIX cron definition (five fields), so Quartz-style six- or seven-field expressions are not accepted. | 0 0 1/1 * * |
| Status | Create / Edit → Details | Only ACTIVE rows are read by the trigger. | none — required |
| Event properties (JSON) | Edit → Json | Passed to the job as its event properties map. | the selected processor’s own property_json, else {} |
| Execution strategy | Not exposed in the UI | How the queued event is executed. The backend fills it with INSERT_TO_QUEUE when the row does not carry one. | INSERT_TO_QUEUE |
Feature visibility / permissions
bl_applet_client_side_perm_dfn holds zero rows for schedulerApplet (checked 2026-09-14), and the applet checks no HIDE_* / SHOW_* keys, so the Feature Visibility screen has nothing to gate.
Server-side, SchedulerCrontabController guards its endpoints with AkaunTenantPermissions.API_TNT_DM_SCH_CRONTAB_READ, _CREATE, _UPDATE and _DELETE (with _OWNER and _ADMIN as the broad grants). Note that Run is guarded by API_TNT_DM_SCH_CRONTAB_CREATE, not by update — anyone who may add a schedule may also fire one immediately.
Fields
Create → Details
| Field | Meaning | Required | Notes |
|---|---|---|---|
| Scheduler Code | The job processor’s queue code | Yes | Read-only input; click it to open Select Processor. Saved upper-cased and trimmed. |
| Scheduler Name | A label for the row | Yes | Also saved upper-cased and trimmed. |
| Status | ACTIVE or INACTIVE | Yes | Only ACTIVE rows are triggered. |
Cron Expression tab — the ngx-cron-editor control, standard flavour. Its value is copied onto the row on submit.
Json tab (Edit) — a single event_properties field holding a JSON object, pre-filled and uploadable from a file.
Listing columns — Scheduler Code, Scheduler Name, Status, Last Run Date, Created Date, Updated Date.
Lifecycle and effects
No document type, no signums, no journal, no stock. The applet writes bl_sch_crontab_hdr; the platform writes bl_sch_crontab_event and the queue rows.
What happens on each pacemaker tick (MainTenantProcessor.triggerSchCron):
- Read every ACTIVE row from the tenant’s cron table. A tenant database that cannot be reached is logged and skipped, not failed.
- Keep only rows whose
coderesolves to aJobProcessorClassNameenum constant —JobProcessorClassName.valueOf(code)must succeed. A row naming a processor the platform does not have is silently dropped: no error, no event, no change to Last Run Date. - Keep only rows that are due.
SchedulerCronService.isTimeToTriggerClockparses the expression with the UNIX cron definition, takeslast_run_timeas the reference (falling back tocreated_date), asks the parser for the next execution after that point, and fires when now is after it. Because the reference is the last run and not the wall clock, a tenant that was down catches up on exactly one missed occurrence per tick. - For each due row, check
jobExistsInQueuefirst — an identical job already queued is logged and not queued again. - Queue the job with the row’s
event_propertiesas its properties map and the row’sexecution_strategyas its strategy. - Only if the queue insert succeeded: set
last_run_timeto now and write abl_sch_crontab_eventrow with the trigger time. A failed insert leaveslast_run_timealone so the next tick retries.
What “Run” does instead. POST …/sch/crontabs/scheduler/execute/backoffice-ep/{guid} writes an app_scheduler_queue row (start now, expire in five hours, priority 5) carrying the schedule’s code and event properties, then resolves the processor class and calls processAvailableEvent directly. It returns JOBPROCESSORCLASS_CAST_ERROR when the code does not resolve to a usable processor and EVENT_PROCESSING_ERROR when the processor throws. It does not update last_run_time, so a manual run does not shift the next scheduled occurrence.
What the backend fills in. On create, the data-consistency object supplies a GUID, status = ACTIVE, created_date, updated_date, a revision and execution_strategy = INSERT_TO_QUEUE when they are missing, and it defaults last_run_time to now. That last default is the one to remember: a new schedule’s first firing is computed forward from the moment you created it, never from the past, so creating a daily job at 3 pm does not back-fill this morning’s run.
Validation on create rejects a row with no code, no name, no cron expression, no status, no revision, or an execution strategy that is not one of the queue’s own strategy values.
Related applets
- Developer SysAdmin Applet — where job processors and their subscriptions are registered, and where a failed job is inspected.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| A schedule never runs and Last Run Date never changes | Its Scheduler Code does not resolve to a job processor the platform knows. The trigger filters those rows out before the cron check, with no error anywhere. | Re-create the row using the Select Processor picker rather than typing a code. |
| A schedule never runs although the code is right | Status is INACTIVE — only ACTIVE rows are read. | Set Status to ACTIVE. |
| The cron expression is rejected, or fires at the wrong time | Expressions are parsed with the UNIX five-field definition. A six- or seven-field Quartz expression (with seconds or a year) will not behave as written. | Use the Cron Expression tab’s editor, which emits five fields. |
| A daily job created this afternoon did not run for today | last_run_time defaults to the creation moment, and the next execution is computed forward from there. | Expected. Press Run once if today’s run is needed. |
| The same job appears to be skipped some ticks | An identical job was still sitting in the queue; jobExistsInQueue suppresses the duplicate and logs it. | Nothing to do — it runs when the queue drains. |
Run reports JOBPROCESSORCLASS_CAST_ERROR | The schedule’s code resolves to something that is not a usable job processor instance. | Re-select the processor. |
Run reports EVENT_PROCESSING_ERROR | The job itself threw. | Look at the job’s own applet or ask support for the processor log. |
| A manual Run did not delay the next scheduled run | Run writes to a separate queue and deliberately leaves last_run_time alone. | Expected. |
| Personalization → Field Settings shows the 404 page | The menu entry points at a route not registered under personalization. | Use Settings → Field Settings (which is itself inert). |
| Nothing in the tenant runs on schedule at all | The pacemaker / MainTenantProcessor is not running for the tenant. | Ask support. |