Appearance
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 want | Set |
|---|---|
| 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.aggregation—count|sum|avg|min|max. The last four needagg_field(the numeric column to fold).group_by_field— the bucket.countgrouped bystatus→ 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 jobsThe 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
| Symptom | Cause | Fix |
|---|---|---|
| job not registered | no AFTER_CREATE register rule shipped | ship it |
sum/avg writes null | agg_field missing | required for all aggregations except count |
| summary rows collide with another config's | no target_key_prefix | namespace each config's keys |
| rollup creates rows instead of updating | wanted the parent-row shape but set a prefix | use target_key_field: "id", blank prefix |