---
name: halo-change-request
description: "Open a change request in HaloPSA / HaloITSM through a discover-preview-confirm-submit workflow, with the confirmed change ticket type cached in Memory. Use when raising a Halo change, opening a CR in Halo, submitting a change ticket, or asking NetClaw to file a change in HaloPSA."
license: Apache-2.0
user-invocable: true
metadata:
{ "openclaw": { "requires": { "bins": ["python3"], "env": ["HALO_BASE_URL", "HALO_CLIENT_ID", "HALO_CLIENT_SECRET"] } } }
---
# Halo Change Request
Open a **change request** in Halo through a strict discover -> preview -> confirm -> submit flow. Halo is **HaloPSA / HaloITSM** (the same product and the same REST API — HaloPSA is the MSP edition, HaloITSM the internal-IT edition). A "change request" in Halo is not a fixed object type: it is simply a **ticket type** ("Request Type") whose numeric id and custom fields are configured per organization — and an org often defines **several** change types (for example a customer-facing change and an internal change). This skill figures out *which* change is being raised, discovers the matching type, confirms it with a human, remembers it **per category**, learns its fields, and gates the single write behind explicit approval.
## MCP Server
- **Server**: `halo-mcp` (NetClaw-authored, `mcp-servers/halo-mcp/`)
- **Command**: `python3 -u mcp-servers/halo-mcp/halo_mcp_server.py` (stdio transport)
- **Auth**: OAuth2 client-credentials — `HALO_CLIENT_ID` / `HALO_CLIENT_SECRET` against `HALO_BASE_URL` (token from `/auth/token`, resource API at `/api`)
- **Python**: 3.10+ · **Dependencies**: `fastmcp`, `httpx`, `python-dotenv`
- **Read/write posture**: **read-only except one gated write** — `halo_create_change_request` is the ONLY tool in the entire server that can mutate Halo, and it previews (no write) unless `submit=true`.
This skill also uses the **Memory MCP** server (`memory-mcp`) to cache the confirmed change ticket type per Halo tenant.
## Available Tools
| Tool | Parameters | What It Does |
|------|------------|--------------|
| `halo_list_ticket_types` | `can_create_only?`, `customer?`, `showcounts?` | Discover ticket types ("Request Types"). Use `can_create_only=true` to find the org's CHANGE type |
| `halo_get_ticket_type` | `ticket_type` | Authoritative field **schema** for a type — required/optional/visible field definitions |
| `halo_list_fields` | `custom_only?` | Master `FieldInfo` catalog (ids <-> names <-> dropdown option values) |
| `halo_get_field` | `field` | One field definition by numeric id, with its lookup values |
| `halo_list_tickets` | `ticket_type?`, `customer?`, `asset_id?`, `status?`, `open_only?`, `search?` | Find a **sample** existing change to copy field patterns from |
| `halo_get_ticket` | `ticket` (numeric id) | Read one populated ticket as a concrete example |
| `halo_create_change_request` | `summary`, `details`, `ticket_type`, `customer?`, `site?`, `user?`, `asset?`, `custom_fields?`, `submit=false` | **THE ONLY WRITE.** Previews the exact POST body unless `submit=true` |
| `halo_list_clients` | `search?` | Resolve a Halo **Client** name to its id (the `customer` param) |
| `halo_list_sites` | `customer?`, `search?` | Resolve a site within a client |
| `halo_list_users` | `customer?`, `site_id?`, `search?` | Resolve the requesting contact/user |
| `memory_get_facts` | `entity`, `key?` | Recall the tenant's change-type catalog (`key="change_ticket_types"`) |
| `memory_record_fact` | `entity`, `key`, `value`, `metadata?` | Merge a confirmed change type ({id, name, category}) into the catalog |
| `memory_record_decision` | `context`, `decision`, `rationale`, ... | Log the human's ticket-type confirmation |
| `memory_invalidate` | `fact_id`, `reason` | Retire a cached ticket type that no longer resolves |
## Key Concepts
- **Halo == HaloPSA == HaloITSM.** Same API; only the packaging differs. Everything here works against either.
- **"Change request" is a ticket type, not a built-in.** Its id and its custom fields are per-org. You must **discover** it (never hard-code it) and **confirm** it with a human before trusting it.
- **There is often more than one change type.** Many orgs split changes — e.g. a **customer** change vs an **internal** change, or standard / normal / emergency. Treat the change type as a *set*: work out *which* change this is (ask if unclear), keep a per-category catalog in Memory, and confirm the specific type before filing.
- **The Halo "Client" is the customer.** Every tool that scopes to a customer org takes it as the `customer` param (a client name or numeric id), which the server resolves to a `client_id`. When someone says "the client," they mean this.
- **Tickets are "Faults"; ticket types are "Request Types"; assets are "Devices"** in Halo's API vocabulary — the tool names hide this, but error text may show it.
- **One write, always gated.** `halo_create_change_request` with `submit=false` performs NO HTTP write; it returns the exact array body that *would* be POSTed to `/api/Tickets`. The real POST happens only on a second call with `submit=true`.
- **Memory holds the *type*, never the *contents*.** Cache only the confirmed ticket-type id and its field metadata — never credentials, never the text of any change ticket.
## Workflow
The full change-request flow, step by step. Do not skip steps (a) or (c).
### (a) Determine the change category, then recall or discover its type
An organization usually has **more than one** change type — most commonly a
**customer** change (raised against a client) and an **internal** change (internal
IT / infrastructure), and sometimes standard / normal / emergency splits. So first
decide *which* change this is, then resolve the matching type. Never assume there is
only one.
1. **Decide the category.** Infer from context: a change scoped to a specific
customer/client is a **customer** change; a change with no client (internal infra,
your own systems) is an **internal** change. **If it is at all ambiguous, ASK the
operator** — e.g. *"Is this a customer change or an internal change?"* Use a short
lowercase label for the category (`customer`, `internal`, `emergency`, ...).
2. **Recall the tenant's change-type catalog:**
```
memory_get_facts(entity="halo-", key="change_ticket_types")
```
The value is a list of `{"id":..., "name":..., "category":...}` — the change types
already confirmed for this instance. `` is from `HALO_TENANT`, or the host
of `HALO_BASE_URL` (e.g. `halo-acme` for `acme.halopsa.com`).
3. **If the needed category is already in the catalog**, validate it still resolves
with `halo_get_ticket_type(ticket_type=)`:
- resolves cleanly -> use that id, go to (b).
- no longer resolves (renamed/deleted) -> drop it from the catalog, re-discover
(below), and `memory_invalidate` the stale fact when you rewrite the catalog.
4. **If the category is missing (or was invalidated), discover:**
```
halo_list_ticket_types(can_create_only=true)
```
This commonly returns **several** change-shaped types (e.g. "Change Request",
"Internal Change", "Emergency Change"). **Present the candidates and ask the
operator which one is the `` change** — do NOT auto-pick when more than
one looks like a change, and do NOT collapse them into a single "the change type".
5. **On confirmation, merge it into the catalog** (add the new category; keep the
others — do not overwrite the whole list with a single entry):
```
memory_record_fact(entity="halo-", key="change_ticket_types",
value=,"name":"","category":""}>)
memory_record_decision(context="Halo change type for / ",
decision="Use ticket type () for changes",
rationale="Operator-confirmed from halo_list_ticket_types(can_create_only=true)")
```
Carry the selected `` into (b) and (c). If a single request genuinely spans both
a customer and an internal change, that is **two** change tickets — run the flow once
per category, confirming each.
### (b) Learn the fields (schema + a real example)
1. **Authoritative schema** — which fields are required vs optional, and their dropdown values:
```
halo_get_ticket_type(ticket_type=)
```
Use `halo_list_fields` / `halo_get_field` to resolve any custom `FieldInfo` ids <-> names <-> option values you need.
2. **A concrete example** — read a real, populated change to see how fields are actually filled:
```
halo_list_tickets(ticket_type=) # find a recent one
halo_get_ticket(ticket=) # inspect its summary/details/customfields
```
3. Cache field metadata alongside the type id (`metadata={...}`) if it helps future runs — but only the metadata, never any ticket's contents.
### (c) Assemble, PREVIEW, then STOP for approval
1. Resolve the customer/site/user as needed (`halo_list_clients`, `halo_list_sites`, `halo_list_users`).
2. **Preview (no write):**
```
halo_create_change_request(
summary="...", details="...", ticket_type="",
customer="", site="...", user=, asset="...",
custom_fields={: , ...},
submit=false)
```
3. **Present the returned preview `body` to the operator verbatim.** It is the exact `/api/Tickets` payload.
4. **STOP. Do NOT submit until the operator explicitly approves this specific preview** (Constitution XIV, Human-in-the-Loop). A vague "go ahead" earlier does not count — approval is per change, on the previewed body.
5. **Only after explicit approval**, re-call the identical arguments with `submit=true` to perform the one real write.
### (d) Record the outcome in GAIT
Log the decision and the created ticket id to the GAIT audit trail (see gait-session-tracking): the confirmed ticket type, the previewed body, the approval, and the resulting Halo ticket id.
## Integration with Other Skills
| Skill | Integration |
|-------|-------------|
| **gait-session-tracking** | **Mandatory.** Record the ticket-type decision, the preview, the human approval, and the created ticket id in the GAIT audit trail |
| **memory** | Caches the per-category change-type catalog per tenant (`entity="halo-"`, `key="change_ticket_types"`); no credentials or ticket contents |
| **halo-ticket-context** | Read the created change and its action history afterward for follow-up |
| **halo-asset-context** | Review the affected asset and its open tickets before proposing the change |
| **servicenow-change-workflow** | Sibling gated-change pattern (STOP-until-confirmed) if the org also runs ServiceNow |
## Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `HALO_BASE_URL` | Yes | Halo host, e.g. `https://.halopsa.com` |
| `HALO_CLIENT_ID` | Yes | OAuth2 client-credentials application id (Configuration > Integrations > Halo API) |
| `HALO_CLIENT_SECRET` | Yes | OAuth2 client secret |
| `HALO_TENANT` | Optional | Tenant identifier (also used to key Memory as `halo-`) |
| `HALO_SCOPE` | Optional | OAuth2 scope (default `all`) |
| `HALO_AUTH_URL` | Optional | Override the auth-server URL for self-hosted layouts |
| `HALO_VERIFY_SSL` | Optional | `true`/`false` TLS verification (default `true`) |
| `HALO_TIMEOUT` | Optional | Per-request timeout in seconds (default 30) |
| `HALO_PAGE_SIZE` / `HALO_MAX_PAGES` | Optional | Pagination tuning (defaults 50 / 20) |
| `HALO_RATE_LIMIT` | Optional | Requests/minute cap (0 = disabled) |
## Important Rules
- **There is exactly ONE write tool** — `halo_create_change_request`. Every other Halo tool is read-only. Never attempt any other mutation through this server.
- **Never submit without explicit, per-change approval.** Always preview first (`submit=false`), present the exact body, STOP, and only re-call with `submit=true` after the operator approves *that* preview (Constitution XIV).
- **Discover the change ticket type(s); never assume it, and never assume there is only one.** An org may have customer / internal / emergency change types. Work out which category applies (ask if unclear), confirm the specific candidate with a human, and keep a per-category catalog. When discovery returns several change-shaped types, present them and let the operator choose — do not auto-pick.
- **Store only the ticket-type id + field metadata in Memory — NEVER credentials, secrets, or the contents of any ticket.** Memory keys off `halo-`.
- **Re-validate the cache.** If the cached type no longer resolves via `halo_get_ticket_type`, `memory_invalidate` it and re-discover.
- **Halo == HaloPSA == HaloITSM** — the workflow is identical for both editions.
- **GAIT logging is mandatory** for the decision and the created ticket id.