Skip to content

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

UpstreamThis appletDownstream
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, statusThe tenant queue, via MainTenantProcessor
Each firingA bl_sch_crontab_event row (the run history) and an updated last_run_time
The Run buttonAn app_scheduler_queue row processed immediately, outside the cron path

Screens and menus

MenuRouteWhat it listsActions
Schedulerschedulerbl_sch_crontab_hdr — Scheduler Code, Scheduler Name, Status, Last Run Date, Created Date, Updated DateAdd, open
Settings → Field Settings, Default Selectionsettings/field-settings, settings/default-selectionSee Configuration
Settings → Webhook, Feature Visibility, Permission Set / User / Team / Rolesettings/…The shared screens
Personalization → Default Selection, Sidebarpersonalization/…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

PrerequisiteWhereWhy
Job processors registered and enabled for the tenantPlatform / BigLedger support; visible in the Developer SysAdmin AppletThe 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 tenantPlatformMainTenantProcessor is what reads the cron table. Nothing in this applet polls.
API_TNT_DM_SCH_CRONTAB_* permissionsSettings → Permission Set / Role PermissionSee 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:

SettingWhereWhat it controlsDefault
Scheduler CodeCreate → Details → Select ProcessorWhich 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 ExpressionCreate / Edit → Cron ExpressionWhen 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 * *
StatusCreate / Edit → DetailsOnly ACTIVE rows are read by the trigger.none — required
Event properties (JSON)Edit → JsonPassed to the job as its event properties map.the selected processor’s own property_json, else {}
Execution strategyNot exposed in the UIHow 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

FieldMeaningRequiredNotes
Scheduler CodeThe job processor’s queue codeYesRead-only input; click it to open Select Processor. Saved upper-cased and trimmed.
Scheduler NameA label for the rowYesAlso saved upper-cased and trimmed.
StatusACTIVE or INACTIVEYesOnly 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):

  1. Read every ACTIVE row from the tenant’s cron table. A tenant database that cannot be reached is logged and skipped, not failed.
  2. Keep only rows whose code resolves to a JobProcessorClassName enum 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.
  3. Keep only rows that are due. SchedulerCronService.isTimeToTriggerClock parses the expression with the UNIX cron definition, takes last_run_time as the reference (falling back to created_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.
  4. For each due row, check jobExistsInQueue first — an identical job already queued is logged and not queued again.
  5. Queue the job with the row’s event_properties as its properties map and the row’s execution_strategy as its strategy.
  6. Only if the queue insert succeeded: set last_run_time to now and write a bl_sch_crontab_event row with the trigger time. A failed insert leaves last_run_time alone 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

SymptomCauseFix
A schedule never runs and Last Run Date never changesIts 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 rightStatus is INACTIVE — only ACTIVE rows are read.Set Status to ACTIVE.
The cron expression is rejected, or fires at the wrong timeExpressions 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 todaylast_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 ticksAn 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_ERRORThe schedule’s code resolves to something that is not a usable job processor instance.Re-select the processor.
Run reports EVENT_PROCESSING_ERRORThe 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 runRun writes to a separate queue and deliberately leaves last_run_time alone.Expected.
Personalization → Field Settings shows the 404 pageThe 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 allThe pacemaker / MainTenantProcessor is not running for the tenant.Ask support.

Related documentation

Last updated on