Hub tree re-sanitized across all ConnectWise skills + references; history rebuilt so the unsanitized security-report blob (007dc40) and the wholesale hub expansion (b42a8e3) are no longer reachable (scrub decision in issue #31 comment 139, option b).
6.0 KiB
6.0 KiB
name, description
| name | description |
|---|---|
| connectwise-open-tickets-robust | Use when API returns old/empty results. Robust ticket fetch. |
ConnectWise Open Tickets (Robust)
This skill provides a reliable method for fetching open tickets from the ConnectWise PSA API, specifically addressing the common issue where the API returns the oldest tickets first, potentially masking active tickets in a large database.
Trigger Conditions
- User requests an "Open Tickets" report for themselves or another technician.
- Previous reports returned 0 results despite known open tickets.
Technical Specifications
- API Endpoint:
https://cw.vistanetinc.com/v4_6_release/apis/3.0/service/tickets - Authentication: Use the
Authorization: Basic base64(companyid+publickey:privatekey)header and a separateclientIdheader. - The "Oldest-First" Pitfall: A simple query for
closedFlag=falsemay return 1,000 results starting from the very first tickets created in the system (many years ago), which may all be closed, leading to an empty result set for newer open tickets.
Execution Workflow
- Initial Fetch: Attempt a query with
closedFlag=falseandpageSize=1000. - Staleness Check: If the returned tickets are all
closedFlag: trueor the ID range is very old (e.g., IDs < 20,000), the API is likely returning the oldest records first. - Robust Recovery: Pivot to an ID-based query. Fetch tickets with
id > [Current_Estimated_Floor](e.g.,id > 110000orid > 119000). - Refined Filtering:
- Ensure
closedFlag == false. - Exclude any status starting with
>(e.g.,>Completed). - Filter by the specific
owner/identifierrequested.
- Ensure
- Formatting:
- Generate a professional HTML table.
- Include: ID, Summary, Status, Company, Date Entered, and SLA Status.
- SLA Color Coding: Green for
isInSla: true, Red forisInSla: false.
- Delivery: Pipe the raw HTML email (including headers) to
himalaya message send -.
Pitfalls & Lessons
- dateEntered is NULL on ticket objects in this deployment (v4_6, seen 2026-09-22) — even on single-ticket GETs. The real entry date lives in the
_infoblock:t['_info']['dateEntered'](alsoenteredBy,lastUpdated,updatedBy). Filtering/conditioning ondateEnteredin list conditions silently matches nothing. slaStatusis a plain string (e.g. "Respond by Fri 09/11 3:10 PM UTC-07"), NOT an object with.name.hoursToSla/dateRespondedmay also be null.isInSla(bool) is the reliable SLA filter.- Do NOT rely on
closedFlagas the only filter if the database is large; always verify the ID range of the returned results. - Ordering: If
sort=dateEntered DESCis not supported or ignored by the API, ID-based filtering is the only reliable way to reach recent tickets. - Himalaya Error: Avoid using files for
himalaya message sendif you encounterindex out of boundspanics; use stdin piping instead.
Ticket Notes & Activity API (learned 2026-09-10)
- Notes endpoint:
GET /service/tickets/{id}/notes. Field isdateCreated, NOTdateEntered— filtering ondateEnteredsilently matches nothing.orderBy=dateEnteredis rejected 400 InvalidOrderBy on this endpoint; sort client-side. - Recent activity often lives in time entries, not notes:
GET /time/entries?conditions=(ticket/id = {id})— time entries DO usedateEnteredand havemember.identifierandnotes. Check both when asked "any recent activity". - Contact-authored notes (email-to-ticket) have no
memberobject — onlycontact. Skip them (and skip the owner's own notes) when counting other-technician activity. - Automated "VNI Bot" notes are a member but not a technician — report them separately from human tech activity.
- A recent-activity ticket may still return 0 notes and 0 time entries yet show
dateResponded/slaStatuson the ticket detail — ticket detail carries NOlastUpdateobject in this deployment (v4_6).
Changing Ticket Owner via API (learned 2026-09-10)
PATCH /service/tickets/{id}requires a JSON Patch array:[{"op": "replace", "path": "owner/id", "value": <memberId>}]— a plain{"owner": {"id": N}}body fails 400 "not in the expected array format"./system/membersis 403 for the standard API key (no security permission). To get a member's numeric id, read any of their notes/ticket objects wheremember: {id, identifier, name}is embedded.- Always verify assignments by re-reading the ticket:
owner.identifiershould match the target member.
Scheduling Ticket Work Blocks (learned 2026-09-10)
Used for the cert-renewal playbook: schedule expiry-minus-7-days, 1h, weekend expiries shift to preceding Friday, stagger same-day blocks by 1h.
- Endpoint:
POST /schedule/entries. Link to ticket viaobjectId(query existing with(objectId = {id})). - Minimal working body:
{name, objectId, member:{id}, where:{id}, dateStart, dateEnd, reminder:{id}, status:{id}, type:{id}, span:{id}, hours}(all times UTC). Returns 201 with created entry. - Known-good values: member
<targetMemberId>; where Remote=4; status Firm=2; type id=4 (identifier 'S'); span id=1 ('N' normal block) or 2 ('B' auto-assign marker); reminder 15min=4. - Auto-assign entries have NO date fields (hours 0.0) — markers only. A dated template entry (has dateStart/dateEnd) is the POST template. Query dated entries with
(dateStart > [YYYY-MM-DDT00:00:00Z]). - Duplicate check first: filter existing entries by objectId for any with
dateStartbefore POSTing; skip if present. A ticket can legitimately need TWO blocks when its summary cert (e.g. GlobalSign Apr 2027) differs from its live cert (e.g. Let's Encrypt Oct 2026) — check what the existing block is for before skipping. - After scheduling, flip ticket status via JSON Patch:
[{"op":"replace","path":"status/id","value":13}](13 = 'Scheduled' on board 1). Verify by re-reading the ticket AND the schedule entry. - Working script:
scripts/schedule_cert_renewals.py(edit TICKETS list; PATCH_STATUS=True also flips ticket status).