Populates historical data so newer inbox features (chiefly server-side inbox
sorting) work on data created before those features shipped. New records are
indexed automatically; only the historical backlog needs this.
Two backfills, run in this order:
| # | Backfill | Service | Populates |
|---|---|---|---|
| 1 | BPM lineage (env-wide) | alpha-jbpm-service |
process_instance_lineage (BPM DB) — subprocess parent resolution |
| 2 | Enquiry projection (per project) | alpha-case-service |
ci_*, ti_*, si_* (Enquiry DB) — the read model the sorted inbox queries |
Both are batched, resumable, idempotent: call the up endpoint repeatedly
until it returns hasMore: false. A crash or timeout is safe — send the same
request again; it resumes from the saved cursor and never duplicates rows.
You need: (1) API access via each service’s Swagger UI or any HTTP client —
every step gives the method, path, and JSON body; (2) a SQL client for the
verification queries (plain SQL with <placeholders>). Optional shell loops are in
Appendix A.
Skip the backfill for a brand-new environment, or when all data was created
after the read-model tables went live. If you only added a sortable case column,
run just the enquiry CASES scope (Step B).
1. Access and prerequisites
1.1 The two services
alpha-jbpm-service— a thin adapter/wrapper in front of the jBPM engine (it
talks to the BPM DB and proxies jBPM’s REST server); not jBPM itself. The
lineage backfill runs here and writes the BPM DB directly.alpha-case-service— owns Case Manager and the enquiry read model. The
enquiry backfill runs here.
Base paths default to bpmservice and caseservice (set per deployment via each
service’s BASE_PATH); replace <domain> with your host. Swagger is at
<domain>/bpmservice/api-docs and <domain>/caseservice/api-docs when
ENABLE_SWAGGER=true (tags Migration — BPM lineage backfill / Migration —
Enquiry projection backfill). All backfill operations are POST.
1.2 Authentication
Both services use the standard IDS client-credentials bearer token — send
Authorization: Bearer <token> on every call (in Swagger, Authorize and paste
it). alpha-jbpm-service, being a jBPM wrapper, also expects user/password
headers to be present; these are normally filled from its JBPM_USER_NAME /
JBPM_PASSWORD, so the bearer token is usually all you send.
1.3 Databases
| Data | Service | Database (env) | Schema (env) |
|---|---|---|---|
BPM lineage (process_instance_lineage, processinstancelog) |
jbpm | BPM_DB_NAME |
ALPHA_BPM_DB_SCHEMA_NAME (default public) |
Enquiry (ci_*, ti_*, si_*, enquiry_config) |
case | ENQUIRY_DB_NAME |
ENQUIRY_DB_SCHEMA_NAME |
| Case Manager (sources, job cursor) | case | CM_DB_NAME |
CM_DB_SCHEMA_NAME |
Typically one Postgres server (shared DB_HOST / DB_PORT / DB_USERNAME /
DB_PASSWORD).
1.4 Confirm the release is installed (asset 1.0.17+)
The endpoints and their DDL ship in asset 1.0.17+, applied by the
migration-service (not by these endpoints). The migration-service writes a
schema_version table into every schema it migrates — including BPM, Enquiry,
and Case Manager (columns service_name, version, applied_at). Confirm a
1.0.17 row in each:
SELECT service_name, version, applied_at
FROM <bpm-schema>.schema_version ORDER BY applied_at DESC LIMIT 10;
-- expect service_name = 'alpha_bpm_service', version = '1.0.17'
Repeat for <enquiry-schema> (alpha_enquiry_service) and <cm-schema>
(alpha_case_service). 1.0.17 ships the BPM DDL that creates
process_instance_lineage; the Enquiry row appears even though this release adds
no enquiry tables. If a schema has no schema_version, migrations weren’t applied
there — fix that first. Then confirm the lineage table exists:
SELECT to_regclass('<bpm-schema>.process_instance_lineage') IS NOT NULL AS exists; -- t
1.5 Enquiry tables for the project
The ci_* / ti_* / si_* tables and their enquiry_config row are created when
the project’s inbox/enquiry config is saved in Studio. Read the (hashed) table
names from enquiry_config — don’t guess:
SELECT "caseType", "caseDataTableName", "taskDataTableName", "signalDataTableName"
FROM <enquiry-schema>.enquiry_config WHERE "projectId" = '<project-uuid>';
Expect one row with all three names. Optionally check whether ti_* has a
parentProcessInstanceId column (needed for the optional TASKS_PARENT_PIID
scope):
SELECT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_schema='<enquiry-schema>' AND table_name='<ti_*>'
AND column_name='parentProcessInstanceId');
1.6 Run order
Run Step A (lineage) first — the enquiry task backfill resolves parents more
accurately when lineage already exists — then Step B (enquiry) per project.
2. Step A — BPM lineage backfill
Environment-wide, on alpha-jbpm-service. All POST; bodies may be {}. Base:
<domain>/bpmservice/migration/bpm-lineage-backfill.
2.1 Baseline — POST /status {}
{ "lineageTableExists": true, "distinctLogProcesses": 125000,
"lineageRows": 0, "missingLineageRows": 125000, "remainingUnits": 48000,
"job": { "status": "idle", "cursorLogId": 0, "lineageWritten": 0, "lastError": null } }
Confirm lineageTableExists: true and record missingLineageRows (the gap).
Cross-check in SQL:
SELECT COUNT(DISTINCT l.processinstanceid) AS missing
FROM <bpm-schema>.processinstancelog l
LEFT JOIN <bpm-schema>.process_instance_lineage pil ON pil.instance_id = l.processinstanceid
WHERE pil.instance_id IS NULL;
2.2 Run the backfill — POST /up
Send {"dryRun":true} first for a no-write preview. Then loop the real request
until hasMore: false:
{ "batchSize": 5000, "maxBatches": 20, "batchDelayMs": 500 }
Each call handles at most batchSize × maxBatches processes. Repeat in Swagger
(Execute → check hasMore → Execute again) or script it
(Appendix A). After each call read
hasMore (false = done), processedThisRun / lineageWrittenThisRun (progress),
and errorsThisRun (should stay 0 — if persistently high, stop and check
service logs).
Request fields, all optional: batchSize (5000), batchDelayMs (500),
maxBatches (20), dryRun (false), clampPageLevels (5000), clampMaxPages
(20). Lower batchSize / maxBatches if you hit gateway timeouts.
2.3 Verify — POST /verify {}
{ "distinctLogProcesses": 125000, "lineageRows": 125000, "missingLineageRows": 0, "ok": true }
Success = ok: true (missingLineageRows: 0). /verify returns 400 if the
table is missing; /status always returns 200. SQL cross-check:
SELECT (SELECT COUNT(DISTINCT processinstanceid) FROM <bpm-schema>.processinstancelog),
(SELECT COUNT(*) FROM <bpm-schema>.process_instance_lineage);
If up reports hasMore: false but missingLineageRows > 0, some parents have no
log row of their own (orphaned history) and can’t become eligible — decide whether
to ignore them.
3. Step B — Enquiry projection backfill (inbox sorting migration)
Per project, on alpha-case-service. caseType is resolved from the project. All
POST. Base: <domain>/caseservice/case/migration/enquiry-projection-backfill.
Scope ALL runs three phases automatically: CASES → TASKS → SIGNALS.
| Scope | Backfills | Use when |
|---|---|---|
ALL (default) |
cases, tasks, signals | normal full backfill |
CASES / TASKS / SIGNALS |
that one only | re-run a single phase |
TASKS_PARENT_PIID |
fills parentProcessInstanceId on ti_* |
optional extra pass, not part of ALL; run after, only if the column exists |
3.1 Baseline — POST /status/project
Body: { "projectId": "<project-uuid>", "scope": "ALL" }
{ "caseType": "loanApplication", "caseManager": { "cases": 8200, "signals": 1500 },
"remainingUnits": 8200, "parentProcessInstanceIdColumnPresent": true,
"job": { "status": "idle", "phase": "CASES", "cursorOffset": 0,
"casesProcessed": 0, "tasksProcessed": 0, "signalsProcessed": 0, "lastError": null } }
Confirm caseType is populated; note caseManager.cases / signals (the totals).
remainingUnits is work left for the current phase; it hits 0 when the scope
finishes.
3.2 Run the backfill — POST /up/project
Preview with "dryRun": true, then loop until hasMore: false:
{ "projectId": "<project-uuid>", "scope": "ALL", "batchSize": 100, "maxBatches": 20, "batchDelayMs": 200 }
phase advances CASES → TASKS → SIGNALS across calls. Watch phase, hasMore,
processedThisRun{cases,tasks,signals}, errorsThisRun.
Fields: projectId (required); scope (ALL); batchSize (100, env
BACKFILL_BATCH_SIZE); batchDelayMs (200, env BACKFILL_BATCH_DELAY_MS);
maxBatches (20); resetCursor (false — set true to restart the scope);
dryRun (false).
TASKS sizing: the TASKS phase iterates cases and loads all of each case’s
tasks, so 100 cases can write far more than 100 task rows. Use a smaller
batchSize(25–50) if cases have many tasks.
3.3 Optional — fill parentProcessInstanceId
Only if the ti_* column exists (§1.5).
After the main backfill, loop until done:
{ "projectId": "<project-uuid>", "scope": "TASKS_PARENT_PIID", "batchSize": 500, "maxBatches": 20, "batchDelayMs": 100 }
3.4 Verify
No separate verify endpoint — re-run /status/project. Done when
remainingUnits: 0 and job.status: "completed". Spot-check row counts (names
from enquiry_config):
SELECT COUNT(*) FROM <enquiry-schema>.<ci_*>; -- ≈ caseManager.cases
SELECT COUNT(*) FROM <enquiry-schema>.<ti_*>;
SELECT COUNT(*) FROM <enquiry-schema>.<si_*>;
4. Pilot, rollback, troubleshooting
4.1 Pilot before a large run
Run a small capped batch, read durationMs (server wall-clock) from the up
response, and extrapolate:
minutes ≈ (total / processedThisRun) × (durationMs / 60000), using
missingLineageRows (lineage) or caseManager.cases (enquiry) as total. Pilot
bodies: lineage {"batchSize":200,"maxBatches":5}; enquiry CASES
{"scope":"CASES","batchSize":100,"maxBatches":10}; enquiry TASKS use 50–100 cases
({"scope":"TASKS","batchSize":50,"maxBatches":2}) since task cost is non-linear.
Then tune batchSize / maxBatches for the full loop.
4.2 Rollback (destructive — non-prod only)
Truncates materialized tables; does not touch source data. POST /down {}
(lineage) truncates process_instance_lineage and resets the job. POST /down/project { "projectId": "<project-uuid>" } truncates the project’s ci_* /
ti_* / si_* (response lists truncatedTables) and deletes its cursor. Verify
counts are 0, then re-run up.
4.3 Troubleshooting
- Timed out / crashed: send the same
uprequest again — it resumes from the
cursor. Check/statusfirst if unsure whether the last batch committed. - Per-row errors: a bad row doesn’t stop the run; watch
errorsThisRunand
job.lastError, check service logs. Stop only if errors stay high every call. - Clamped chains: a
runawayblock (clampedRows, …) means chains deeper than
ALPHA_LINEAGE_MAX_DEPTH(default 31) were capped. Still counts as complete
(ok: truewithclampedRows > 0is fine). Raise the cap and re-run only if you
genuinely have deeper nesting. 400from enquiry: unknownprojectId, missingenquiry_config,
TASKS_PARENT_PIIDwithout the column, or a bad field value.- Concurrency: one
uploop at a time per backfill/project (single cursor).
Lineage and a different project’s enquiry can run together.
5. Reference
5.1 Endpoints (all POST)
- Lineage —
/bpmservice/migration/bpm-lineage-backfill:/status{},
/verify{},/up(fields §2.2),/down{}. - Enquiry —
/caseservice/case/migration/enquiry-projection-backfill:
/status/project{projectId, scope?},/up/project
(fields §3.2),/down/project{projectId}.
5.2 Environment variables (service config)
- Shared DB:
DB_HOST/DB_PORT/DB_USERNAME/DB_PASSWORD. - alpha-jbpm-service:
BASE_PATH(bpmservice),BPM_DB_NAME,
ALPHA_BPM_DB_SCHEMA_NAME(public),ALPHA_LINEAGE_MAX_DEPTH(31),
JBPM_USER_NAME/JBPM_PASSWORD,IDS_ENABLE,ENABLE_SWAGGER. - alpha-case-service:
BASE_PATH(caseservice),CM_DB_NAME/
CM_DB_SCHEMA_NAME(holds theenquiry_projection_backfill_jobcursor),
ENQUIRY_DB_NAME/ENQUIRY_DB_SCHEMA_NAME,BACKFILL_BATCH_SIZE(100),
BACKFILL_BATCH_DELAY_MS(200),IDS_ENABLE,IDS_URL/IDS_CLIENT_ID/
IDS_CLIENT_SECRET,ENABLE_SWAGGER.
Appendix A — optional automation scripts
Not required — the runbook runs from Swagger or any HTTP client. These
bash + curl loops just automate the repeat-until-hasMore:false step.
# Mint IDS token
TOKEN=$(curl -s -X POST "<IDS_URL>/connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=<IDS_CLIENT_ID>" -d "client_secret=<IDS_CLIENT_SECRET>" \
-d "grant_type=client_credentials" | jq -r .access_token)
JBPM=(-H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" -H "user: svc" -H "password: svc")
CASE=(-H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN")
L="<domain>/bpmservice/migration/bpm-lineage-backfill"
E="<domain>/caseservice/case/migration/enquiry-projection-backfill"
PID="<project-uuid>"
# Step A — lineage
while :; do R=$(curl -s -X POST "$L/up" "${JBPM[@]}" -d '{"batchSize":5000,"maxBatches":20,"batchDelayMs":500}')
echo "$R" | jq '{hasMore,processedThisRun,errorsThisRun,durationMs}'
[ "$(echo "$R" | jq -r .hasMore)" = false ] && break; sleep 2; done
# Step B — enquiry (scope ALL; use TASKS_PARENT_PIID for the optional pass)
while :; do R=$(curl -s -X POST "$E/up/project" "${CASE[@]}" -d "{\"projectId\":\"$PID\",\"scope\":\"ALL\",\"batchSize\":100,\"maxBatches\":20,\"batchDelayMs\":200}")
echo "$R" | jq '{phase,hasMore,processedThisRun,errorsThisRun,durationMs}'
[ "$(echo "$R" | jq -r .hasMore)" = false ] && break; sleep 2; done