Skip to content

Add a cross-row aggregation job

What you're doing

"Count / sum / average many rows and store the result somewhere." A nightly inventory reconciliation, a per-asset maintenance-cost rollup, seats-used per licence. No Java. One entity_aggregation_config row, run by the generic engine-entity.aggregation-sweep job, shipped as seed data.

This is the batch cousin of a KPI Data Service: a KPI is computed on demand for a screen; an aggregation job writes a number back into a table on a schedule so other rows/pages/rules can use it.

Two target shapes

You wantSet
a summary entity with one row per bucket (e.g. count of equipment per status)target_key_field = a text key column, target_key_prefix to namespace it
a value written onto the source's own parent row (e.g. synced_cost onto a maintenance record)target_key_field: "id" — the bucket key is the parent's primary key

The complete example — count per status into a summary entity

spk-assembly/metadata/seed-data/office-equipment-aggregation-configs.json (standalone example, real file):

json
{
  "entity": "entity_aggregation_config",
  "keyFields": ["sweep_code"],
  "source": "office-equipment",
  "rows": [
    {
      "sweep_code": "equipment-status-reconciliation",
      "source_entity": "oeq_equipment",
      "aggregation": "count",
      "group_by_field": "status",
      "target_entity": "oeq_reconciliation",
      "target_key_field": "metric_key",
      "target_key_prefix": "equipment_status:",
      "target_value_field": "metric_value",
      "target_timestamp_field": "last_reconciled_at",
      "seeded_by": "office-equipment",
      "active": true
    }
  ]
}

You also ship: the oeq_reconciliation summary entity (metric_key text, metric_value numeric, last_reconciled_at datetime), the shared entity_aggregation_config entity file (idempotent-additive), and an AFTER_CREATE register rule (below).

Line by line

  • sweep_code — unique id for this config; the natural key.
  • source_entity — the table to fold.
  • aggregationcount | sum | avg | min | max. The last four need agg_field (the numeric column to fold).
  • group_by_field — the bucket. count grouped by status → one number per status value.
  • when_status_in / status_field — optional filter: only fold rows in these statuses.
  • target_entity — where results go.
  • target_key_field + target_key_prefix — for each bucket, the target row is keyed <prefix><bucket value> (e.g. equipment_status:AVAILABLE). Prefix lets several configs share one summary table.
  • target_value_field — the column that receives the number.
  • target_timestamp_field — stamped with the run time.

The "write onto the parent row" variant

json
{
  "sweep_code": "maintenance-cost-rollup",
  "source_entity": "oeq_maintenance_cost",
  "aggregation": "sum",
  "agg_field": "total_cost",
  "group_by_field": "maintenance_id",
  "target_entity": "oeq_maintenance",
  "target_key_field": "id",
  "target_value_field": "synced_cost",
  "target_timestamp_field": "cost_synced_at",
  "active": true
}

Here group_by_field values are oeq_maintenance primary keys, so the sum is written straight onto each maintenance row. Leave target_key_prefix blank.

The register rule

Ship spk-assembly/metadata/rules/ensure_aggregation_sweep_job_registered.json:

json
{
  "entityType": "entity_aggregation_config",
  "name": "ensure_aggregation_sweep_job_registered",
  "triggerEvent": "AFTER_CREATE",
  "conditions": null,
  "actions": "[{\"type\": \"EXECUTE_SERVICE\", \"service\": \"ensureEntityAggregationSweepJobRegistered\"}]",
  "priority": 10,
  "active": true
}

Ground yourself first

bash
erp schema pull entity-aggregation-config
erp examples patterns --kind jobs

The cross-row-aggregation-into-summary-entity and target-key-field-id-rollup patterns each point at a real shipped file.

How to verify it worked

bash
erp api get "/api/v1/jobs/engine-entity.aggregation-sweep"
# → { "status": "ENABLED", ... }

erp api post "/api/v1/jobs/engine-entity.aggregation-sweep/execute" --body "{}"
erp api get "/api/v1/entities/oeq_reconciliation/records/query?size=10"
json
{ "rows": [
  { "metric_key": "equipment_status:AVAILABLE", "metric_value": 8, "last_reconciled_at": "2026-09-10T..." },
  { "metric_key": "equipment_status:ASSIGNED",  "metric_value": 4, "last_reconciled_at": "2026-09-10T..." }
], "total": 2 }

Common mistakes

SymptomCauseFix
job not registeredno AFTER_CREATE register rule shippedship it
sum/avg writes nullagg_field missingrequired for all aggregations except count
summary rows collide with another config'sno target_key_prefixnamespace each config's keys
rollup creates rows instead of updatingwanted the parent-row shape but set a prefixuse target_key_field: "id", blank prefix