2.6 KiB
.id, never .timer.id — using the wrapped pattern silently returns null and creates duplicate timers when the script "retries".
type: feedback
Rule: When parsing the response from POST /tickets/{id}/timer_entry, use .id directly — the response is a FLAT object. Do NOT use .timer.id // .timer_entry.id.
Verified response shape (2026-05-05, ticket #32253):
{
"id": 39031258,
"ticket_id": 109895882,
"user_id": 1750,
"start_time": "2026-05-05T09:00:00.000-07:00",
"end_time": "2026-05-05T09:30:00.000-07:00",
"recorded": false,
"billable": true,
"notes": "...",
"product_id": 26118,
"comment_id": null,
"ticket_line_item_id": null,
"active_duration": 1800,
"billable_time": 1800
...
}
Why: The skill doc at .claude/commands/syncro.md shows
TIMER_ID=$(echo "$TIMER_RESP" | jq -r '.timer.id // .timer_entry.id')
That fallback resolves to null because neither key exists on the flat response. A null TIMER_ID then breaks charge_timer_entry ("Not found"). If the script retries the timer_entry POST after the perceived failure, it creates a duplicate — Syncro has no idempotency. Hit this on ticket #32253 (Cascades) on 2026-05-05; created two duplicate 0.5hr timers and had to delete one via delete_timer_entry before charging.
How to apply:
- Parsing: Always
jq -r '.id'on the timer_entry response. - After ANY ambiguous timer_entry response (null
.id, jq error, network blip): GET the ticket and inspect.ticket.ticket_timers[]BEFORE retrying. Filter forrecorded: falseentries with the start/end times you just sent. - Cleanup if duplicates exist:
POST /tickets/{id}/delete_timer_entrywith{"timer_entry_id": N}for the older duplicate(s). Returns{"success": true}. - Verifying the timer is on the ticket:
GET /tickets/{id}→.ticket.ticket_timersis the authoritative list. The standalone/ticket_timers?ticket_id=Nquery parameter does NOT filter by ticket — returns the entire global timer history.
Charge timer response is also flat:
{"id": 39031258, "recorded": true, "ticket_line_item_id": 42313052, ...}
Parse as .ticket_line_item_id to get the auto-generated line. Do not look for a wrapper.
Where this lands in skill code: .claude/commands/syncro.md example block needs .id not .timer.id // .timer_entry.id. Until the skill is patched, override the example pattern when running.