Skip to content

Add a scheduled reminder job

What you're doing

"Flip this record's status once a date passes" — or "N days before it passes". Overdue checkouts, expiring warranties, upcoming audits. You write no Java and no job class. The platform ships one generic job, engine-entity.status-date-sweep, that runs daily and applies every entity_status_date_sweep_config row. Adding a reminder = adding one config row, shipped with your plugin as seed data.

How the generic job works

Once a day (cron 0 5 0 * * *, per tenant) the job:

  1. reads every active entity_status_date_sweep_config row;
  2. for each, lists all rows of that config's entity_name;
  3. for every row whose status_field is one of when_status_in and whose date_field satisfies compare_op against now (optionally shifted by day_offset days), it sets set_field (defaults to status_field) to set_status_to.

Because a swept row no longer matches when_status_in, re-running the job is a harmless no-op — it's naturally idempotent.

The complete example

spk-assembly/metadata/seed-data/office-equipment-sweep-configs.jsonreal file:

json
{
  "entity": "entity_status_date_sweep_config",
  "keyFields": ["entity_name", "date_field", "set_status_to", "day_offset"],
  "source": "office-equipment",
  "rows": [
    {
      "entity_name": "oeq_checkout",
      "status_field": "status",
      "when_status_in": "CHECKED_OUT",
      "date_field": "due_date",
      "compare_op": "lte",
      "day_offset": 3,
      "set_status_to": "DUE_SOON",
      "active": true,
      "seeded_by": "office-equipment"
    },
    {
      "entity_name": "oeq_checkout",
      "status_field": "status",
      "when_status_in": "CHECKED_OUT,DUE_SOON",
      "date_field": "due_date",
      "compare_op": "lt",
      "day_offset": 0,
      "set_status_to": "OVERDUE",
      "active": true,
      "seeded_by": "office-equipment"
    }
  ]
}

You also need the shared config entity to exist. Ship it (idempotent-additive): spk-assembly/metadata/entities/entity_status_date_sweep_config.jsonreal file.

Line by line

The seed-data wrapper

  • entityentity_status_date_sweep_config, the shared platform config table.
  • keyFields — the natural key. On re-install, a row matching all of these is updated (only if a value changed) rather than duplicated. Include day_offset so the 3-day and 0-day rows for the same date column are treated as distinct.
  • source — your plugin id; stamped into seeded_by.
  • rows — the config rows.

Each config row

  • entity_name — the entity to sweep.
  • status_field — which column holds the lifecycle status (default status).
  • when_status_in — comma-separated statuses a row must currently be in to be eligible.
  • date_field — the date/timestamp column to compare against now.
  • compare_oplte | lt | gte | gt.
  • day_offset0 = compare against now exactly. 3 = compare against now + 3 days, i.e. "fires when due_date is within 3 days" — this is the "N days before" reminder. Negative shifts earlier.
  • set_field — the column to write. Omit it to write status_field itself.
  • set_status_to — the new value.

The two rows together

  • Row 1: a CHECKED_OUT checkout whose due_date is within 3 days → DUE_SOON.
  • Row 2: a CHECKED_OUT or DUE_SOON checkout whose due_date has passed → OVERDUE.

This two-row shape (a "soon" warning then an "it happened" flip) is the exact pattern the platform's own leave_policy sweep uses.

Flip a status, not a boolean

Write to an enum/text status column, as above. Writing set_status_to: "true" to a boolean set_field currently fails inside the generic job with a SQL type error — the job passes the value as a string. Model reminders as status values (DUE_SOON, OVERDUE) rather than boolean flags until that is fixed. (This was found while building this tutorial.)

The job registers itself

The platform ships an AFTER_CREATE rule on entity_status_date_sweep_config that calls ensureEntityStatusDateSweepJobRegistered — so the first config row your plugin seeds auto-registers the job for your tenant. You don't ship that rule.

Ground yourself first

bash
erp schema pull entity-status-date-sweep-config
erp schema pull plugin-seed-data
erp examples patterns --kind jobs

How to verify it worked

After publishing, the job is registered and enabled:

bash
erp api get "/api/v1/jobs/engine-entity.status-date-sweep"
json
{ "jobCode": "engine-entity.status-date-sweep", "status": "ENABLED",
  "cronExpression": "0 5 0 * * *", "concurrencyPolicy": "PER_TENANT" }

Run it on demand and check a record flips. This exact sequence was run against the tutorial module:

bash
# a checkout that is CHECKED_OUT with a due_date in the past
erp api post "/api/v1/entities/oeq_checkout/records" \
  --body '{"checkout_number":"CO-100","equipment_id":1,"employee_id":1,"checkout_date":"2026-08-01","due_date":"2026-09-04","status":"CHECKED_OUT"}'

erp api post "/api/v1/jobs/engine-entity.status-date-sweep/execute" --body "{}"
# → { "executionId": 534 }

erp api get "/api/v1/jobs/engine-entity.status-date-sweep/executions?size=1"
# → resultJson: {"swept": 4, "failed": 0, "rowsScanned": 102, "configsScanned": 30}

erp api get "/api/v1/entities/oeq_checkout/records/2"
# → { "checkout_number": "CO-100", "status": "OVERDUE" }

CHECKED_OUTOVERDUE. Verified.

Common mistakes

SymptomCauseFix
job execution reports "failed": Nset_field is a boolean columnflip a status value instead
nothing sweepswhen_status_in casing doesn't match the enummatch exactly
"N days before" never firesforgot day_offset (defaults to 0)set day_offset to the window
re-install duplicates the configkeyFields don't uniquely identify the rowinclude enough fields (add day_offset, set_field)
job not registeredplugin seeded no config row, or the config entity wasn't shippedship entity_status_date_sweep_config.json and at least one seed row