Management Studio REST API (export / import / workflow instances)¶
The Management Studio SPA (the Angular app at …/ManagementStudioPrtyDev) is backed by a
plain REST API. The endpoints were reverse-engineered from the app's own JS bundle
(main.js) using the authenticated session from export_from_management_studio.py. This
page documents the three surfaces we drive programmatically and the Python tools that wrap
them, so agents can import packages and read workflow run errors without screenshots.
All calls reuse the same auth/session as the export tool (.env MS_USERNAME/MS_PASSWORD,
auto-login, x-xsrf-token). Base URL: https://testmobile.lincolnshirehp.com/ManagementStudioPrtyDev.
Response envelope: success responses are {StatusCode, Value, …} and the SPA unwraps to
.Value (replicated by unwrap() in both tools). Errors are {StatusCode, Message, Warning}
or {Detail} / problem-details. A wrong endpoint path returns the SPA's index HTML with
HTTP 200 — never treat a non-JSON 200 as success.
1. Export (export_from_management_studio.py)¶
GetAll* list endpoints → api/RefData/PrepareExportPackage (base64 payload) →
api/RefData/DownloadExportPackage. Per-category packages (a combined ~60 MB package 504s
during assembly). See deployment/dev-to-live.md and §15 of CLAUDE.md.
2. Import (ms_import.py) — a 3-call flow¶
versaa-rag\.venv\Scripts\python.exe versaa-rag\ms_import.py <package.ftpackage>
--inspect-only # upload + show manifest, do NOT import (safe preview)
--activate # import AND activate immediately (default: import as INACTIVE/draft)
--clear # cancel a staged import on the server
- Upload — tus 1.0.0 resumable to
<BASE_URL>/files. The SPA uses tus-js-client (endpoint:"files", 1 MB chunks,overridePatchMethod). We hand-roll it:POST /fileswithTus-Resumable: 1.0.0+Upload-Length+Upload-Metadata→ read theLocationheader → send bytes viaPOST <Location>withX-HTTP-Method-Override: PATCHandUpload-Offset.fileId= last path segment of the upload URL (this is the SPA'suploadedPackageId). - Inspect —
POST api/RefData/UploadImportPackage/?id=<fileId>(null body) → manifest preview, e.g.{Forms:[…], Workflows:[{Name, Version, Comment, …}], …}. - Import —
POST api/RefData/ImportCurrentPackage/?id=<fileId>&activate=<True|False>(null body) → an array of per-item results:Cancel instead:{ "Successful": true, "ErrorMessage": null, "ItemName": "Unvented Hot Water Storage", "ItemType": "Form" }POST api/RefData/ClearCurrentImportPackage.
Gotchas¶
- Default is
activate=False— versions land inactive/draft; nothing goes live until you--activate(or activate deliberately in MS). Safe staging. - Forms auto-version on import (MS assigns the next sequential version), so re-importing a form succeeds. Workflows do not — re-importing an existing workflow version returns a generic HTTP 500 ("An unexpected error occurred"); a workflow import must be a new version. Inspect succeeds either way (the package parses); the 500 is in the apply step.
- Import/activation errors are fully visible in the per-item
ErrorMessage. Runtime workflow errors are NOT — see §3.
3. Workflow instances (ms_workflow_errors.py) — read-only¶
Lets an agent see workflow runs (the "Workflows → Search Instances → Instance Details" screen) as JSON. This is the only way to see runtime workflow errors — they occur when an instance executes against QL, long after import, and are invisible to the import flow.
versaa-rag\.venv\Scripts\python.exe versaa-rag\ms_workflow_errors.py list # default: Errored, last 24h
--status Errored|Running|Complete|any --since 24h|7d --workflow "<name>" --limit N --json
versaa-rag\.venv\Scripts\python.exe versaa-rag\ms_workflow_errors.py show <instanceId> [--no-xml] [--json]
- List:
GET api/Workflows/GetAllWorkflowInstances(query:workflowName,workflowInstanceStatuses,filterStatusByCategoryStatuses,completedBefore/After,externalId,initialDataValue,startIndex,sortColumn,sortDirection,maxItems). Returns{Items:[…], TotalCount}. Items:Id, WorkflowName, Status, UserStatus, StartedAt, LastTransitionedAt, CompletedAt, ErrorMessage, ExternalId, Tags. NB errored instances have noCompletedAt(0001-01-01…placeholder) so the tool sorts/filters onStartedAt.workflowInstanceStatusesis mandatory — the API 500s with "WorkflowInstanceStatuses has to be set to perform a search" if omitted, so--status anysends the full enum, not nothing. - Detail:
GET api/Workflows/GetWorkflowInstanceInfo?workflowInstanceId=<id>&includeTasks=true→{ WorkflowInstanceInfo:{…, InitialData:"<Event…> Task XML", ErrorMessage, WorkflowVersion}, WorkflowHistory:[{Name, TypeName, Status, Result, RunCount, LastError, …}], WorkflowTasks, … }.showprints the errored step, full exception/stack, step history, and the initial Task XML.
Status enum¶
Running, Errored, Complete, Aborting, Aborted, Cancelling, Cancelled, PendingEvent,
PendingRetry, PendingWorkflowInstance, AbortedWithError, RollbackPendingRetry.
Mutation actions — ms_workflow_actions.py (WRITE companion)¶
The viewer (§3) stays read-only; instance mutations live in a separate tool:
versaa-rag\.venv\Scripts\python.exe versaa-rag\ms_workflow_actions.py retry|abort|cancel <instanceId> [--yes]
retry → GET api/Workflows/RetryCurrentStep?workflowInstanceId=<id> (re-run the current/errored step)
- abort → POST api/Workflows/AbortWorkflowInstance?workflowInstanceId=<id> (hard stop → Aborted)
- cancel → POST api/Workflows/CancelWorkflowInstance?workflowInstanceId=<id> (cancel + rollback)
Without --yes it only previews (prints current state); --yes fires the live Dev mutation,
then re-reads the state. Abort/cancel are async — the instance transitions
(Errored/Running → Aborting → Aborted) a moment after the call returns True. (Verified
18/06/2026: aborted two errored test instances → Aborted.) UpdateInstanceToActiveVersion
exists too but isn't wired. Workflow version activation/rollback uses a different route:
POST api/Workflows/SetActiveVersion?workflowId=<id>&version=<n>. See
workflows/fix-errors.md for the manual UI path.
4. Trigger a workflow / test a form (ms_submit_form.py)¶
Tests the form-data → workflow path without completing a form on the Aareon Mobile device: trigger a workflow on Dev from a Task/Event XML, then read the resulting instance's outcome.
versaa-rag\.venv\Scripts\python.exe versaa-rag\ms_submit_form.py --from-instance <id> [--dry-run]
... --xml <file.xml> --workflow "<name>" [--external-id X] [--no-watch] [--watch-timeout N] [--json]
- Trigger:
POST /api/Workflows/StartWorkflowbody{WorkflowName, ExternalId, InitialData}(the<Event…><Task…>…XML). Response is the new instance Id (the SPA navigates to its details page with it). The tool also tags a uniqueExternalIdand can locate the instance by it viaGetAllWorkflowInstances?externalId=if needed, then renders the outcome via the §3 viewer. - Modes:
--from-instancecapturesWorkflowName+InitialDatafrom an existing instance and replays it;--xml+--workflowsubmits a hand-built/edited Task XML. --dry-runprints the exact payload and posts nothing — the approval gate before any live run.
Gotchas / safety¶
- LIVE: a non-
--dry-runrun executes a REAL workflow on Dev (QL writes, emails, document generation, DW writes). Always--dry-runfirst; pick a side-effect-light workflow. StartWorkflowstarts a NAMED workflow directly — it bypasses the device's event-routing andTask.Taskpersistence. Great for testing one workflow against specific form data; for the fully faithful device path use the deferred WCF SOAP route (IdentityService/UserLogin→ITaskQueueService/SendTaskEventDataattestmobile.lincolnshirehp.com:9006/PrtyDev, namespacehttp://schemas.1sttouch.com/2009/01/Mobile/).- An invalid
WorkflowNamereturns a clean500 "Workflow '<name>' does not exist"and runs nothing — handy plumbing check. - Verified (18/06/2026): triggering
IS Test Survey Completioncreated instance51a4e432-…and the tool read its livePendingRetry/step-error back — proving the submit→run→read loop.
5. Workflow briefing — structure + runtime (ms_workflow_info.py) — read-only¶
"Learn a workflow before you change the form/workflow that touches it." Given a name, prints
its structure (trigger messages, ordered steps with type + key params, and each step's
transitions incl. XPath branch conditions) plus a runtime summary (status breakdown +
recent instances, with an optional drill-in). Structure is read live, falling back to the
locally-extracted .workflow XML (clearly flagged) when MS is unreachable.
versaa-rag\.venv\Scripts\python.exe versaa-rag\ms_workflow_info.py "<name>"
--structure-only | --runtime-only --version active|latest|<N> (default active)
--local # force local extracted XML, skip MS
--runs N # recent instances to summarise (default 10)
--sample errored|latest # also drill into one instance (reuses the §3 viewer)
--json
- Resolve:
GET api/RefData/GetWorkflowsWithVersions→[{Info:{Id,Name,ActiveVersion, LatestVersion,…}, Versions:[…]}]. Name match is case-insensitive (exact first, else substring); a partial name matching >1 workflow lists candidates with their active/latest versions instead of guessing. - Structure (live):
GET api/WorkflowEditor/GetLatestWorkflowDefinition?workflowId=<id>orGET api/WorkflowEditor/GetWorkflowDefinition?workflowId=<id>&version=<n>&lockForEdit=false→{Id,Name,Description,Steps:[{Id,Name,Type,Parameters:[{Name,Value,Type}], Transition:{GatewayType,Targets:[{TargetId,Type,Label,Expression,Value}]}}], Starts:[{Type: ManualStart|MessageStart, MessageName}], Ends:[…]}. The JSON mirrors the on-disk.workflowXML 1:1, so one normaliser renders either source. - Structure (local fallback): newest
extracted/workflows/Workflows/<Name>_<ver>.workflow(spaces→+in the filename). Prints a[fallback: local XML, may be stale — last Dev pull …]banner so a stale read is never mistaken for live. - Runtime: reuses §3's
GetAllWorkflowInstances(status breakdown + recent runs) and, with--sample,GetWorkflowInstanceInforendered by the §3 viewer. Remember completed instances are cleared, so a healthy workflow often legitimately shows no instances. - Read-only: no Save/SetActiveVersion/Start endpoints are wired (those live in §3's
ms_workflow_actions.py/ §4'sms_submit_form.py).
6. Users / accounts / properties (ms_user_info.py) — read-only¶
Answers the "duplicate accounts / which operative sees which jobs" class of ticket. Lists a person's account(s), shows one account's user-properties, and lists the property definitions.
versaa-rag\.venv\Scripts\python.exe versaa-rag\ms_user_info.py find "Walker" # accounts for a person (flags duplicates)
... show "LINCOLNSHIREHP\WalkerA" # one account's user-properties + capabilities
... props # the configurable property definitions
... find "Walker" --env live --i-understand-live # real operative data lives in LIVE
- List:
GET api/Users/GetUsers→[{DisplayName, UserName, Properties:[…], Capabilities:{}}].Propertiesis empty here (GetUsers does NOT inline property values) and it ignores search/paging params, so filtering is client-side. Not capped at a small number (Live returns all ~406 users); still verify counts with--json. - Per-user detail (property VALUES):
GET api/Users/GetUser?id=<UserName>(the SPA'sGetUserDetails; param isid= the UserName, e.g.LINCOLNSHIREHP\WalkeA) →{UserName, DisplayName, Properties:[{Name,Value}], Groups, AssignedPermissions, IsEnabled, IsLockedOut, …}.showuses this to print real values + enabled/locked + groups. - Property definitions:
GET api/Users/GetConfigureProperties→ 33 definitions. There is no literal "Work Team" property — job visibility is driven byAppointment Pools,DiaryResource/DiaryResourceGroup,Operative Trades(andAareon_OperativeID/QL User Id). ("Work Team" in it/appointment-polling.md is shorthand for these.) - Real operative property values live in Live (Dev operative accounts are unconfigured), so
diagnosing a visibility ticket means a read-only
--env livelookup. Live MS web UI is onmobile.lincolnshirehp.com(nottestmobile) and needs Live web access (1TPG_PrtyLive_Access_*). - Full Users route map (from
main.js): readGet{Users,User,UsersWithGroupSupport, UserListWithAccessSystemPermission,Groups,GroupUsers,Permissions,ConfigureProperties}; write (NOT wired){Add,Delete,Enable}User,UpdateUserProperty,Set*Permissions,*Group*.
7. Read-only SQL (ms_sql.py)¶
Run SELECTs against the Versaa/QL databases directly — no SSMS round-trip. Ships the
sql/cheat-sheet.md queries as named presets plus ad-hoc --sql/-f.
# Launch via the runas helper so the connection authenticates as the SQL admin account:
versaa-rag\run_as_sql_admin.ps1 whoami
versaa-rag\run_as_sql_admin.ps1 tasks-by-user --user WALKERA
versaa-rag\run_as_sql_admin.ps1 tasks-by-form --form "Fire Risk Assessment"
versaa-rag\run_as_sql_admin.ps1 --db ql contact-check --prop B23546
versaa-rag\run_as_sql_admin.ps1 tables --like hpm1st # schema discovery
versaa-rag\run_as_sql_admin.ps1 columns --table hpm1stoa
versaa-rag\run_as_sql_admin.ps1 --sql "SELECT TOP 5 name FROM sys.databases" --json
- Auth = Windows / integrated (SSMS connects as
LINCOLNSHIREHP\adminis). pyodbc uses the process identity, sorun_as_sql_admin.ps1doesrunas /netonly /user:LINCOLNSHIREHP\adminis(password prompted, never stored) and then runs the tool. Runningms_sql.pydirectly uses your login (LINCOLNSHIREHP\SaleeI), which gets Login failed (18456) on these servers. - Driver: "ODBC Driver 18 for SQL Server" (auto-picked); Driver 18 →
Encrypt=yes; TrustServerCertificate=yes(internal hosts have no trusted cert). - Env→DB map (
--env, default dev; non-dev needs--i-understand-live): dev =lhp-sql02/Versaa_PrtyDev(+a_qlfdatvia--db ql); live =aareon-sql01/1stTouch_PrtyLive(+QLFDAT).--dbtakesversaa/ql/an explicit catalog. - Read-only guard: every statement must start
SELECT/WITH;INSERT/UPDATE/DELETE/DROP/ ALTER/EXEC/…and multi-statement injection are rejected before connecting. Output: aligned table (default),--json,--csv.
8. Ticket intake (halo_ticket.py + extract_ticket.py)¶
extract_ticket.py <N>now reads ascratch/halo_tickets/ticket_<N>/folder containing mixed files, and supports.emlemails (stdlibemail: base64/quoted-printable decode, text/plain preferred else HTML→text, attachments extracted) and standalone images — on top of DOCX/ODT/PDF. (Added because ticket threads arrive as.eml; no more hand-decoding base64.)halo_ticket.pydrives your authenticated Halo web session with Playwright (LHP has no Halo API). Persistent profile (versaa-rag/.halo_profile, git-ignored) → sign in once incl. MFA:Needspython versaa-rag/halo_ticket.py login # one-time, headed python versaa-rag/halo_ticket.py fetch 20472 # headless: saves page text/PDF/PNG + attachments, then runs extract_ticketHALO_BASE_URL=https://lhp-servicehub.haloitsm.com+HALO_TICKET_PATH=tickets?id={n}in.env. Read-only (navigates/screenshots/downloads only). Halo renders the ticket conversation inside an iframe (the main frame only has the queue chrome + header), so the tool readsinner_textfrom every frame and concatenates — that's what captures the message bodies. The full-page PDF/PNG always capture the ticket too (the screenshot is a reliable fallback). Verified end-to-end on ticket 20472.
Discovering more endpoints¶
versaa-rag/tools/discover_import_endpoint.py logs in, downloads the SPA bundles to
versaa-rag/tools/_ms_js/, and greps them for api/… routes. Re-run it to map new surfaces
(the file is one minified line — use char-offset context windows, not grep -C).
Related¶
- deployment/dev-to-live.md — Dev → Test → Live process
- workflows/fix-errors.md — fixing/retrying workflow errors (UI + SQL)
- sessions/gotchas-and-tips.md — export automation gotchas