Appearance
Data and environments
This page documents the four Administration screens that move data or definitions: Data import (CSV into an entity), Tenant migration (move a tenant to another region), Environment promotion (copy an application or module from one tenant to another) and Test / QA cases (versioned test definitions that can run HTTP checks). For the permission summary see Administration and DevOps overview. For routes that are not on these screens (plugins, AI bundles, integrations, Engagement) see Command line - move definitions between environments.
Data import
Data import loads rows from a CSV file into any active entity defined in the Entity Designer. You map CSV columns to entity fields, preview, then run. Every row is created through the same Entity Engine path as a manual create, so field constraints and declarative rules apply. Administrators and implementers use it for initial loads. See Entities for the entities themselves.
Where to find it: Workspace > Administration > Data import. Page key data-import; the screen title reads "Data Import".
Key concepts
| Term | Meaning |
|---|---|
| Target entity | An entity with status active. The select shows its label, or its name when it has no label. |
| Target field | An importable field of the entity: active, not a system field, not read-only. A field marked required shows an asterisk in the mapping select. |
| Mapping | For each CSV column, the target field it fills, or "- skip this column -". |
| Suggested mapping | Computed on preview by matching each column header to a field name or label, ignoring case, spaces, underscores and hyphens. |
Fields and options
Source panel:
| Field | Type or values | Default | Required | Description |
|---|---|---|---|---|
| Select target entity | Select of active entities | none | Yes | Changing it clears the preview and result |
| Choose CSV file | File picker, .csv | none | Yes | Shows the file name once chosen. Maximum 10 MB |
| Preview | Button | n/a | n/a | Enabled when both entity and file are set. Shows a spinner while loading |
Mapping panel (after Preview). Title: "Column mapping - N data row(s), M of K columns mapped".
| Column | Description |
|---|---|
| Source column | The CSV header |
| Target field | Select of target fields (label, with * when required) or "- skip this column -" |
| Sample values | The values of that column in the first rows, comma separated (hidden on narrow screens) |
Result panel ("Import result"): chips "N succeeded" and, when any failed, "N failed"; a grid of failed rows with Row and Error (search "Search errors"). When all rows succeeded the panel states "All rows imported successfully."
Actions
| Action | Effect | API |
|---|---|---|
| Preview | Parses the header and counts data rows; returns up to 5 sample rows, the target fields and the suggested mapping | POST /api/v1/entities/{name}/import/preview (multipart file) |
| Run Import | Imports every data row using the current mapping. Disabled until at least one column is mapped | POST /api/v1/entities/{name}/import/execute (multipart file and mapping) |
mapping is a JSON array of { "sourceColumn": "...", "targetField": "..." | null }.
Procedures
Import customers from customers.csv:
text
Customer Name,E-mail,City,Credit Limit
Acme Ltd,billing@acme.example,Pune,50000
Globex,ap@globex.example,Leeds,- Open Data import, select the entity
Customer, choosecustomers.csv, select Preview. - The panel reads "Column mapping - 2 data row(s), 4 of 4 columns mapped" because the headers match field labels. Change "E-mail" to the field
emailif it was left unmapped. - Select Run Import. The result shows "2 succeeded".
- If a row fails, for example a missing required field, the grid lists
Row 3with the validation message from the entity.
Statuses and lifecycle
Each row ends as success or failure; there is no persisted import job. Rows already created are not rolled back when a later row fails.
Permissions
The import controller itself performs no permission check. Each row is created by the Entity Engine as the acting user, so entity-level rules, role-field rules and BEFORE_CREATE / AFTER_CREATE rules apply and may reject rows.
API and CLI
bash
erp api get /api/v1/entitiesThe multipart import calls are made from Studio or an HTTP client; erp api sends JSON bodies only.
Limits and behavior
- CSV only. The reader is deliberately simple: UTF-8, first line is the header, one record per line, cells split on every comma, surrounding double quotes removed. A quoted cell that contains a comma is split incorrectly. Blank lines are skipped. XLSX is not supported.
- Maximum file size 10 MB.
- Blank cells are not sent, so field defaults apply. All values are sent as text and converted by the Entity Engine.
- Row numbers in errors match a spreadsheet: the header is row 1 and the first data row is row 2.
- There is no de-duplication or update: every row calls create. Importing the same file twice creates duplicate records unless the entity has a unique constraint.
- The server does not check that a mapped field exists on the entity; an unknown field name is passed to the Entity Engine, which decides whether the row is rejected. The request itself is rejected only when no column is mapped.
Errors and troubleshooting
| Message or symptom | Cause | Fix |
|---|---|---|
| "Pick a target entity and a CSV file first." | Preview pressed without both inputs | Choose both |
file is required | Empty file | Choose a file with content |
File too large (max 10MB) | Over the limit | Split the file |
CSV file is empty | No header line | Add a header row |
At least one column must be mapped to a target field | Everything skipped | Map at least one column |
No such entity: x | Entity renamed or deleted since the page was opened | Reload and reselect |
"mapping" must be a valid JSON array of {sourceColumn, targetField} | Malformed mapping in an API call | Correct the JSON |
| Row error text from the entity | Validation or rule failure for that row | Fix the value in the CSV and import only the failed rows again |
Tenant migration
Tenant migration moves the current tenant to a different region. It takes a real backup, restores it into a new database for the destination region, verifies row counts, then repoints routing. Background job scheduling for the tenant is paused while the migration runs. Platform operators and tenant owners use it, normally during a planned window.
Where to find it: Workspace > Administration > Tenant migration. Page key tenant-migration.
Key concepts
| Term | Meaning |
|---|---|
| Region | A deployment region registered in the region registry (GET /api/v1/authoring/regions) with a code, a name, an Active flag and an Is-primary flag. |
| Home region | The region that currently hosts the tenant's data. |
| Assignment status | active (normal), migrating (a migration is running), suspended (a cutover failed after a verified restore). |
| Run | One migration attempt, identified by a UUID; its steps are recorded with the run. |
| Data residency | The tenant's declared residency constraint; a target region that violates it is rejected before anything is touched. |
Fields and options
Section Current:
| Element | Description |
|---|---|
| Home region | The code of the home region |
| Status | Chip: green active, amber migrating, red otherwise |
| Database identifier | The database name of the tenant, or "(default)" |
Section Migrate to a different region:
| Field | Type or values | Default | Required | Description |
|---|---|---|---|---|
| Target region | Select of active regions other than the home region, shown as "Name (code)" with " - primary" for the primary region | none | Yes | "No other active region available" when there is none. Disabled during a migration |
| Start Migration | Button | n/a | n/a | Shows "Migrating..." and a spinner while the synchronous call runs |
Section Migration history (title adds " - run" and the first 8 characters of the run ID): one row per step with a chip and the detail text, a timestamp and the backup record number when present. Step chips:
| Step | Chip label | Meaning |
|---|---|---|
started | Started | Migration requested |
backed_up | Backed up | A real backup of the source was taken (the detail names the backup record) |
restored | Restored into destination | The dump was restored into the new database tenant_<id>_<region>_db |
verified | Verified (row counts matched) | Per-table row counts match the baseline |
cutover | Cutover (routing repointed) | the tenant's database and region now point to the destination |
completed | Completed | Migration finished |
failed | Failed | The run stopped; the detail explains why |
A Refresh button reloads the history. The text "No migration has been attempted for this tenant yet." shows when the history is empty. When the assignment is suspended a warning states that a previous migration's cutover step failed after a verified restore and that manual intervention is required.
Actions
| Action | Effect | API |
|---|---|---|
| Start Migration | Runs the whole migration and returns the final outcome. Success shows the result message; failure shows "Migration did not complete: message" | POST /api/v1/authoring/tenant-region/{tenantId}/migrate with {"targetRegion":"sg-east"} |
| Refresh / load | Loads regions, assignment and history | GET /api/v1/authoring/regions, GET /api/v1/authoring/tenant-region/tenant, GET /api/v1/authoring/tenant-region/{tenantId}/migration-status |
Procedures
Move tenant 7 from in-east to sg-east:
- Announce a maintenance window. Take and verify a backup first in Backup.
- Open Tenant migration. Current shows Home region
in-east, Statusactive. - Choose Target region "Singapore East (sg-east)" and select Start Migration.
- When the call returns, the history shows Started, Backed up, Restored into destination, Verified, Cutover and Completed. Current shows Home region
sg-east. - Check Jobs and Monitoring to confirm scheduled work resumed.
Statuses and lifecycle
| Assignment status | Meaning | Transitions |
|---|---|---|
active | Normal | migrating when a run starts |
migrating | A run is in progress; the job scheduler skips the tenant; another run is refused | active on success or on a backup or verification failure (original assignment restored); suspended if the cutover write fails |
suspended | Cutover failed after a verified restore | Manual intervention by an operator |
Run order: refuse invalid requests, append started, claim the region-ownership lease (30 minutes), mark the tenant migrating, back up, restore into the new database, verify, grant the read-only replica role on the new database, repoint the tenant's database and region, evict cached connections, mark active, append completed, release the lease.
Permissions
region:tenant-migration / execute. Without it the endpoint answers 403 with an empty body and the screen shows the HTTP error. The reads of regions, assignment and status are not permission-checked by the tenant-region controller.
API and CLI
bash
erp api get /api/v1/authoring/regions
erp api get /api/v1/authoring/tenant-region/tenant
erp api post /api/v1/authoring/tenant-region/7/migrate --body '{"targetRegion":"sg-east"}'
erp api get /api/v1/authoring/tenant-region/7/migration-statusResult body:
json
{
"success": true,
"tenantId": 7,
"runId": "5b0b1c8e-4c0a-4a4e-9e11-8e5c1d3a9f10",
"sourceRegion": "in-east",
"targetRegion": "sg-east",
"finalStatus": "active",
"backupRecordId": 42,
"message": "migration in-east -> sg-east completed for tenant 7"
}A failed migration returns HTTP 422 with the same shape and success: false; a rejected request returns 400 with {"error": "..."}.
Limits and behavior
- The call is synchronous: the HTTP request stays open for the full backup, restore and verification time.
- The destination database is created on the same database host and port as the source's registered location, named
tenant_<id>_<region code with dashes replaced by underscores>_db. The source database is not modified or dropped. - Verification compares per-table row counts with the baseline captured at backup time. Tables with vector columns are excluded from the comparison and noted in the result.
- On a backup failure, a verification mismatch or a failed restore, the tenant is returned to
activein its original region and no routing change is made. - The read-only replica role is granted select on the new database; if that grant fails a warning is logged and replica-served reads may fail until it is granted manually.
- The migration needs the backup engine in the same process. A deployment that has backups switched off refuses it.
Errors and troubleshooting
| Message or symptom | Cause | Fix |
|---|---|---|
| "Choose a target region first." | Start pressed without a selection | Select a region |
targetRegion is required | API call without a body field | Supply targetRegion |
target region 'x' is not a known, active region | Unknown or inactive region | Choose from the list |
tenant N has no region assignment yet - assign a home region first | No assignment | Operator assigns a home region |
tenant N already has a migration in progress | Assignment is migrating | Wait; if stuck, operator investigates |
tenant N is already in region 'x' | Target equals home | Choose another region |
target region 'x' violates tenant N's declared data residency: reason | Residency rule | Choose a compliant region |
could not acquire the region-ownership lease for tenant N - currently held by 'node' until time | Another node holds the lease | Retry after the lease time |
backup failed for tenant N (backup id=B) - source untouched, rolling back to active | Backup failure | See Backup |
restore verification FAILED for tenant N: ... | Row counts differ | Do not retry blindly; contact the operator |
cutover step failed AFTER a successfully verified restore into 'db' - tenant left SUSPENDED, manual intervention required | Routing write failed | Operator repoints routing or restores the assignment |
cross-region tenant migration needs the backup service on this deployment | Backup slice not co-hosted | Run from a deployment that hosts it |
Environment promotion
Environment promotion copies an Application or a Module, with all the artifacts it groups, from one tenant (typically a sandbox) into another tenant (typically production). You preview a diff first; nothing is written until you select Promote. Implementers and release managers use it. Rollback is not a button here: every promoted artifact arrives through the target tenant's normal draft and version history.
Where to find it: Workspace > Administration > Environment promotion. Page key environment-promotion.
Key concepts
| Term | Meaning |
|---|---|
| Source tenant | The tenant the definitions are exported from. |
| Target tenant | The tenant they are imported into. |
| Owner | The Application or Module being promoted, identified by type and name. |
| Package | The encrypted application package (.erpapp) engine used internally. It carries the authored definitions only, never records or runtime data. Types included: pages, forms, blocks (widgets), themes, dashboards, rules, actions, entities, websites, website pages, menus, CMS collections, portals. |
| Sandbox | A tenant flagged as part of the environment chain, with an optional environment label and a default promotion target. |
| Diff status | new (absent in the target), changed (differs from the target), unchanged. |
Fields and options
| Field | Type or values | Default | Required | Description |
|---|---|---|---|---|
| Source tenant id | Text (numeric) | empty | Yes | Tenant to read from |
| Target tenant id | Text (numeric) | empty | Yes | Tenant to write to; also selects which promotion history is shown |
| Owner type | Select: Application, Module | Application | Yes | Wire values application and module |
| Owner name | Text | empty | Yes | Exact name of the Application or Module in the source tenant |
| Known environment-chain tenants | Chips "Name (#id, label)" | listed when sandboxes exist | n/a | Selecting a chip sets the Source tenant id |
Diff panel (title: owner type and name, with a status chip for the owner itself):
| Column | Description |
|---|---|
| Type | Artifact type |
| Name | Artifact name |
| Status | Chip new, changed or unchanged; the suffix "(existence only)" means the comparison could only confirm existence, not content |
Empty diff text: "No child artifacts (the owner's own definition is the only content to promote)."
History panel "Promotion history into tenant N": columns When, From tenant (#id), Owner (type and name), By; newest first.
Actions
| Action | Effect | API |
|---|---|---|
| Preview Diff | Exports the owner from the source tenant and compares with the target, writing nothing | GET /api/v1/authoring/environment-promotion/diff?sourceTenantId=&targetTenantId=&ownerType=&ownerName= |
| Promote | Imports the owner into the target tenant, then records a log row and refreshes the history. Result banner: Promoted "name" - N created, N new version(s), N skipped (entity collisions). | POST /api/v1/authoring/environment-promotion/promote with sourceTenantId, targetTenantId, ownerType, ownerName |
| Refresh (history) | Reloads the history | GET /api/v1/authoring/environment-promotion/history?targetTenantId= |
| Configure sandbox (API only) | Flags a tenant as a sandbox with a label and a default promotion target | POST /tenants/{id}/sandbox with {"isSandbox":true,"environmentLabel":"UAT","promotionTargetTenantId":3}; list with GET /tenants/sandboxes |
Procedures
Promote the Application Field Service from UAT (tenant 4) to production (tenant 3):
- Open Environment promotion. Select the chip "UAT (#4, UAT)" or type
4into Source tenant id. - Enter Target tenant id
3, Owner type Application, Owner nameField Service, and select Preview Diff. - Review the table, for example Page
work-order-listchanged, Formwork-ordernew, EntityWorkOrderunchanged. Confirm the owner chip. - Select Promote. The banner reads
Promoted "Field Service" - 5 created, 3 new version(s), 1 skipped (entity collisions).and the history gains a row. - In tenant 3, review the new drafts and publish them. To undo one, open its designer and use Restore this version on the earlier version.
Statuses and lifecycle
Promoted artifacts arrive as drafts or new draft versions in the target tenant and follow the normal lifecycle in Versioning and release management. A name collision in the target creates a new draft version next to the untouched previous one. A colliding entity is skipped (reported as "skipped") rather than altered.
Permissions
For the diff and promote calls the actor needs AUTHOR on the owner type in the source tenant and PUBLISH on the owner type in the target tenant. Viewing history needs PUBLISH on application in the target tenant. Otherwise the request is denied with Actor "x" lacks AUTHOR on application for tenant T.
API and CLI
bash
erp api get "/api/v1/authoring/environment-promotion/diff?sourceTenantId=4&targetTenantId=3&ownerType=application&ownerName=Field%20Service"
erp api post /api/v1/authoring/environment-promotion/promote --body '{"sourceTenantId":4,"targetTenantId":3,"ownerType":"application","ownerName":"Field Service"}'
erp api get "/api/v1/authoring/environment-promotion/history?targetTenantId=3"Promote response (created, newVersion, skippedExisting are lists of {type, name, version}):
json
{
"ownerCreated": false,
"created": [ { "type": "form", "name": "work-order", "version": 1 } ],
"newVersion": [ { "type": "page", "name": "work-order-list", "version": 4 } ],
"skippedExisting": [ { "type": "entity", "name": "WorkOrder", "version": 1 } ]
}Limits and behavior
- Cross-tenant by design: the export runs in the source tenant's context and the import in the target's.
- The package is a blueprint of authored definitions. Records, sessions and execution history are never copied.
- For types other than those compared by content, the diff is existence-only; such rows are marked "(existence only)".
- Each promote writes one log row (
environment_promotion_log): source and target tenant, owner, import result, actor, time. - The screen requires all four inputs: "Source tenant, target tenant, and owner name are all required."
Errors and troubleshooting
| Message or symptom | Cause | Fix |
|---|---|---|
| "Source tenant, target tenant, and owner name are all required." | Missing input | Fill all four |
ownerType must be "application" or "module", got: x | API call with another type | Use application or module |
Failed to prepare promotion diff / Failed to promote (HTTP 500) | Package encryption or decryption error | Retry; check the server log with erp logs tail |
Actor "x" lacks PUBLISH on application for tenant T | Missing target permission | Grant publish on the target tenant |
| An artifact is missing after promotion | It is not grouped under the Application or Module | Assign it to the owner in the source tenant and promote again |
Test / QA cases
Test / QA cases stores versioned test case definitions and runs the HTTP kind for real. A test case has a target type, a target reference and an ordered list of steps with expected results. Cases of type api execute a real HTTP request per step and check status and body; other types are structured, versioned documentation of a test procedure and record a run as not executable. QA engineers and implementers use it. For the broader testing picture see Testing.
Where to find it: Workspace > Administration > Test / QA cases. Page key test-cases.
Key concepts
| Term | Meaning |
|---|---|
| Test case | An artifact (test_case_definition) with the lifecycle draft, published, deprecated, archived. |
| Target type | api, page, workflow or manual. Only api is executed. |
| Step | One action with parameters and expected results. |
| Run | One execution, stored in test_run with outcome, step results, the test case version, the user and start time. |
| Outcome | passed, failed, error, not_executable. |
Fields and options
List panel "Test cases": each row shows the name and "vN - status". New opens the editor.
Editor:
| Field | Type or values | Default | Required | Description |
|---|---|---|---|---|
| Name | Text (create only) | empty | Yes | "Name is required" otherwise |
| Definition JSON | Multi-line JSON | Template with targetType api, empty targetRef and one GET step | Yes | Must parse ("Definition must be valid JSON") |
| Chips | "vN - status" and the target type | n/a | n/a | Current state |
Definition schema:
| Property | Type | Description |
|---|---|---|
targetType | api, page, workflow, manual | Anything other than api is not executed. A missing value is treated as manual |
targetRef | String | Free reference to the thing under test (URL, page or workflow name) |
steps[].action | String | For api: the HTTP method (GET, POST, PUT, DELETE, PATCH; default GET). For other types: free text |
steps[].params.url | String | Required for api steps; must be an absolute URL reachable from the server |
steps[].params.headers | Object of strings | Optional request headers |
steps[].params.body | JSON | Optional; sent as application/json |
steps[].expected.status | Integer | Optional expected HTTP status |
steps[].expected.bodyContains | String | Optional text that the response body must contain |
Example:
json
{
"targetType": "api",
"targetRef": "Order API health and create",
"steps": [
{
"action": "GET",
"params": { "url": "https://erp.example.com/api/v1/health" },
"expected": { "status": 200, "bodyContains": "UP" }
},
{
"action": "POST",
"params": {
"url": "https://erp.example.com/api/v1/entities/SalesOrder/records",
"headers": { "Authorization": "Bearer TOKEN", "X-Tenant-Id": "7" },
"body": { "customer": "Acme Ltd", "total": 1200 }
},
"expected": { "status": 200 }
}
]
}Run result panel ("Run result" with an outcome chip, green for passed, red for failed or error): one line per step, "Step N (action): mark detail". Run history panel lists up to the stored runs with outcome chip, start time, "vN" and the user.
Actions
| Action | Shown when | Effect | API |
|---|---|---|---|
| Create | New | Creates a draft | POST /api/v1/authoring/test-cases |
| Save draft | Existing | Saves with the revision shown | PUT /api/v1/authoring/test-cases/{id}/draft |
| Publish | Status draft | Publishes the version | POST /api/v1/authoring/test-cases/{id}/publish |
| Run | Existing | Executes and stores a run; refreshes history | POST /api/v1/test-cases/{id}/runs |
| (API only) | Run history | GET /api/v1/test-cases/{id}/runs | |
| (API only) | New version, clone, deprecate, archive, delete, history | POST .../{id}/new-version, /clone, /deprecate, /archive, DELETE .../{id}, GET .../name/{name}/history |
The screen is a lightweight list-and-editor; it does not provide the full designer toolbar (publish, clone and history for all versions are reachable through the API and CLI).
Procedures
Check an endpoint after a deployment:
- Select New, name it
orders-smoke. - Paste the example definition above, adjusting the URL, and select Create.
- Select Run. The result shows
Step 1 (GET): ... HTTP 200and the outcome chippassed. - Select Publish to freeze version 1. A later change requires a new version.
Statuses and lifecycle
Artifact status: draft to published, then deprecated or archived. A published version is immutable: saving it fails with test_case definition N is PUBLISHED - published definitions are immutable; create a new version instead. A concurrent edit fails with Draft N was saved by someone else: expected revision R but the row is at C.
Run outcome:
| Outcome | Meaning |
|---|---|
passed | Every step passed |
failed | At least one step did not match its expectations and none raised a request error |
error | At least one step raised a request error (the call itself failed) |
not_executable | The target type is not api; the run is recorded with no steps |
Step details: HTTP 200; HTTP 500 - expected status 200; HTTP 200 - body did not contain "UP"; step has no params.url; request failed: message.
Permissions
AUTHOR on test_case to create, edit, clone, create versions and run; PUBLISH to publish, deprecate, archive and delete. Reading the list, definitions and run history is open.
API and CLI
bash
erp artifact list --type test-cases
erp artifact create --type test-cases --name orders-smoke --file orders-smoke.json
erp artifact publish --type test-cases --id 18
erp api post /api/v1/test-cases/18/runs
erp api get /api/v1/test-cases/18/runsThe same artifact operations are available as erp plugin op artifact-list --type test-cases and the related artifact-* operations.
Limits and behavior
- Requests are sent by the server, so the URL must be reachable from it. No authentication is added unless you set it in
params.headers; do not store long-lived secrets in a definition, which is readable by anyone who can read test cases. - The HTTP client treats an HTTP status of 400 or above as a request failure. The step is then recorded as
request failed: ...and the run outcome iserror, so an expectation of an error status (for exampleexpected.status404) cannot pass. - No timeout is configured on the HTTP client.
- The definition is stored as written; structure is not validated on save.
- Only
apicases run; there is no browser automation for page or workflow cases.
Errors and troubleshooting
| Message or symptom | Cause | Fix |
|---|---|---|
| "Definition must be valid JSON" | Syntax error | Fix the JSON |
| "Name is required" | Empty name on create | Enter a name |
step has no params.url | url missing | Add params.url |
request failed: ... | Network failure, DNS, TLS or HTTP error status | Check the URL from the server; expect error for 4xx and 5xx |
not_executable | targetType is not api | Use api, or treat the case as documentation |
