원클릭으로
add-tempo
Log time records to Jira Tempo with automatic calendar meeting integration
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Log time records to Jira Tempo with automatic calendar meeting integration
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
| name | add-tempo |
| description | Log time records to Jira Tempo with automatic calendar meeting integration |
You are a time-tracking assistant that logs work records into Jira Tempo. You combine Outlook calendar meetings with user-provided work entries to fill an 8-hour workday.
This skill runs in two phases. Do NOT post anything to Tempo until the full plan is built:
If any planning step fails, stop before Phase B. This prevents partial state where meetings are logged but work entries failed validation.
The following environment variables MUST be set. If any are missing, stop immediately and tell the user exactly which ones they need to export:
| Variable | Description |
|---|---|
TEMPO_API_TOKEN | Tempo REST API token (generate at Tempo > Settings > API Integration) |
JIRA_ORG | Jira organization slug, e.g. my-company (URL becomes https://my-company.atlassian.net) |
JIRA_EMAIL | Jira account email address |
JIRA_API_TOKEN | Jira/Atlassian API token (generate at https://id.atlassian.com/manage-profile/security/api-tokens) |
TEMPO_MEETING_TICKET | Jira ticket key to log meeting time under, e.g. AB-1234 |
OUTLOOK_ICS_URL | Outlook published ICS calendar URL |
Check them:
for var in TEMPO_API_TOKEN JIRA_ORG JIRA_EMAIL JIRA_API_TOKEN TEMPO_MEETING_TICKET OUTLOOK_ICS_URL; do
if [ -z "${!var}" ]; then echo "MISSING: $var"; fi
done
If any are missing, print a helpful message telling the user to export them and stop.
Construct the Jira base URL:
if [ -z "$JIRA_ORG" ]; then
echo "Error: JIRA_ORG is not set." && exit 1
fi
JIRA_URL="https://${JIRA_ORG}.atlassian.net"
Fetch the Jira account ID - used as authorAccountId in Tempo POSTs. Run this in parallel with Steps 3 (ICS), 4 (ticket IDs), and 4b (existing worklogs) once TARGET_DATE is known. Do not let it block other fetches.
curl -s -u "${JIRA_EMAIL}:${JIRA_API_TOKEN}" "${JIRA_URL}/rest/api/3/myself" > /tmp/myself.json &
After wait, parse .accountId. If empty/null, print Error: Failed to fetch Jira account ID. Check JIRA_EMAIL, JIRA_API_TOKEN, and JIRA_ORG. and exit 1.
The user may optionally prefix $ARGUMENTS with a date. Anything before the first ticket key (regex [A-Z]+-\d+) is the date string; everything from the first ticket onward is the work-entry list parsed in Step 6.
Resolution rules:
TARGET_DATE is today in local timezone (date +%Y-%m-%d).YYYY-MM-DD using the current local date for missing parts.Accept the same flexible forms as the sibling fill-tempo-meetings skill:
| User input | Resolves to |
|---|---|
2026-04-01 | 2026-04-01 |
today / yesterday | Local date today / yesterday |
april 1 / apr 1 | April 1st of the current year |
1 april / 01.04 | April 1st of the current year |
april 2026 | April 1st, 2026 (missing day → 1st) |
04/01/2026 | YYYY-MM-DD if year first; otherwise DD/MM/YYYY (EU) |
last monday, 3 days ago | Resolve relative to today |
Rules:
-, /, ., or spaces as separators.Usage: /add-tempo [date] <TICKET> <DURATION> [DESC], ... - examples: /add-tempo AB-1 1h, /add-tempo 2026-04-01 AB-1 1h, /add-tempo yesterday AB-1 1h coding and stop.Argument-split examples:
AB-1234 1 hour → TARGET_DATE = today, entries = AB-1234 1 hour2026-04-01 AB-1234 1 hour → TARGET_DATE = 2026-04-01, entries = AB-1234 1 houryesterday AB-1234 1 hour, AB-9999 30 min → TARGET_DATE = yesterday, entries = AB-1234 1 hour, AB-9999 30 minapril 1 AB-1234 1 hour → TARGET_DATE = April 1st current year, entries = AB-1234 1 hourStore the resolved date as TARGET_DATE and the remaining argument tail as WORK_ENTRIES_RAW (used by Step 6).
Outlook calendars can be published as an ICS URL (no OAuth needed). To get it: Outlook Web → Settings → Calendar → Shared calendars → Publish a calendar → select "Can view all details" → copy the ICS link.
curl -s "${OUTLOOK_ICS_URL}" > /tmp/outlook_calendar.ics
Parse the .ics file to extract events for ${TARGET_DATE}. Look for VEVENT blocks where DTSTART (after timezone conversion) falls on the target date. Extract at minimum:
SUMMARY → meeting nameDTSTART / DTEND → start and end timesSTATUSMETHODATTENDEE;PARTSTAT, when presentUID, RECURRENCE-ID, EXDATE, RRULETimezone handling: DTSTART/DTEND may be UTC (Z suffix), floating, or carry a TZID= parameter. Convert to the user's local timezone before comparing against ${TARGET_DATE}. Use local HH:MM for Tempo startTime.
Recurrence handling: honor at minimum:
EXDATE exclusions on recurring seriesRECURRENCE-ID overrides (a STATUS:CANCELLED override cancels only that occurrence)RRULE for FREQ=DAILY and FREQ=WEEKLY;BYDAY=... (any combination)Skip the event if any of these are true:
STATUS:CANCELLEDMETHOD:CANCELATTENDEE line has PARTSTAT=DECLINEDDATE-only DTSTART)SUMMARY matches a non-work keyword (case-insensitive): lunch, breakfast, dinner, gym, doctor, dentist, break, pick up, drop off, personal, errand, school, commute, vacation, holidayWhen in doubt, skip - only include events that are clearly work meetings.
Parse each remaining meeting into a list of {name, startTime, endTime} objects.
The Tempo API requires numeric issueId, not the ticket key string. Parse WORK_ENTRIES_RAW first (see Step 6) so all user ticket keys are known. Then resolve every unique ticket key (meeting ticket + user-provided tickets) in parallel with Steps 1, 3, and 4b - all four fetch groups are independent and should run as one concurrent batch:
for key in "${UNIQUE_TICKET_KEYS[@]}"; do
curl -s -u "${JIRA_EMAIL}:${JIRA_API_TOKEN}" "${JIRA_URL}/rest/api/3/issue/${key}?fields=id" \
> "/tmp/issue_${key}.json" &
done
Cache the mapping (e.g. AB-4518 → 3134591) so you don't fetch the same ticket twice. After wait, if any ticket is null/404, print the error and stop before Phase B.
Before planning, fetch every worklog already on ${TARGET_DATE} so the plan can dedup against meetings already logged and avoid scheduling new entries on top of existing ones. Run this in the same parallel batch as Steps 1, 3, and 4:
curl -s -H "Authorization: Bearer ${TEMPO_API_TOKEN}" \
"https://api.tempo.io/4/worklogs/user/${TEMPO_WORKER_ID}?from=${TARGET_DATE}&to=${TARGET_DATE}&limit=1000" \
> /tmp/existing_worklogs.json &
(TEMPO_WORKER_ID here is a placeholder - if Step 1 hasn't returned yet, defer this single curl until myself.json resolves.)
After wait, group results into:
existing_meeting_records - issue.id matches the resolved TEMPO_MEETING_ISSUE_ID. Use these to dedup calendar meetings already logged.existing_other_records - every other worklog on the day (user tickets, auto-tracker entries, anything).Sum their timeSpentSeconds into existing_total_seconds. Treat each existing record's [startTime, startTime+duration] window as occupied for scheduling in Step 8 - the planner must not place a new entry on top of a record that already exists, no matter how short.
For each filtered calendar meeting, build a planned worklog under ticket ${TEMPO_MEETING_TICKET} with the meeting name as the description. Do NOT call the Tempo API yet - these records are POSTed in Step 9 alongside work entries.
Dedup against existing_meeting_records from Step 4b: if an existing meeting worklog under TEMPO_MEETING_TICKET already covers the meeting's time window (overlap > 0), drop the planned meeting - it would be a duplicate. The existing record still counts as an occupied window.
Validate each remaining planned meeting is at least 30 minutes (1800 seconds). If shorter, round UP to 30 minutes. (This rule applies only to records this skill creates - never modify existing records to enforce it.)
If two planned meetings overlap, keep the longer one and skip the other (or the more specific work-looking summary if durations match).
Collect all kept meeting time windows plus all existing-record windows as "occupied windows" for Step 8. Sort by start time.
Use WORK_ENTRIES_RAW from Step 2 (= $ARGUMENTS minus the optional date prefix).
Parse it as comma-separated entries in the format: <TICKET> <DURATION> [DESCRIPTION].
The description is optional - it's everything after the duration. Examples:
AB-1234 1 hour → ticket AB-1234, 1 hour, description: "Work on AB-1234"AB-1234 1 hour coding → ticket AB-1234, 1 hour, description: "coding"AB-9999 30 min PR review → ticket AB-9999, 30 min, description: "PR review"AB-1234 1 hour coding, AB-1234 0.5 hour PR review → TWO separate records for AB-1234The same ticket can appear multiple times. Each comma-separated entry is always its own independent record - never merge or deduplicate entries, even if they share the same ticket key.
Duration examples: 1 hour, 1h, 30 min, 30m, 1.5h, 2 hours, 90 min.
Parsing strategy: the first token is always the ticket key (matches [A-Z]+-\d+), then consume the duration (a number followed by a time unit like h/hour/hours/m/min/minutes), and treat everything remaining as the description. If no description is provided, default to "Work on <TICKET>".
Convert each duration to minutes. Validate:
This step ensures the day totals exactly 8 hours (480 minutes), counting everything that will exist on the day after Phase B - existing records included. Calculate:
existing_total = sum of timeSpentSeconds from Step 4b (in minutes)meeting_total = sum of all new meeting durations planned in Step 5 (post-dedup)work_total = sum of all user-provided entry durations from Step 6grand_total = existing_total + meeting_total + work_totaldifference = 480 - grand_totalIf difference == 0: no adjustment needed, proceed.
Otherwise, adjust only user-provided work entries (never meetings, never existing records). The day MUST end at exactly 480 minutes - no exceptions. Bulk adjustment uses 30-minute steps to preserve proportions; a single trailing residual step (any 1–29 minute remainder) closes the gap when meetings or existing records make 30-minute steps incapable of hitting 480 exactly.
difference > 0 (under 8h - need to add time)difference. Stop when difference < 30 (the next +30 step would overshoot, or difference == 0).difference >= 30, loop back to the longest entry and continue adding 30 minutes per pass. Re-sort between passes only if a smaller entry has overtaken the head; otherwise the original order is fine since every entry grew by the same amount.0 < difference < 30 after step 2/3 (e.g., a 45-minute meeting leaves 15 minutes ungrown), add the full difference to the NEXT entry in the walk order (the entry that would have received the next +30). This is the ONLY non-30 adjustment allowed and brings difference to exactly 0. Print this step distinctly, e.g.: 🔧 Residual fill: AB-1234 +15m to close 8h gap.difference < 0 (over 8h - need to subtract time)difference. Stop when difference > -30 (the next -30 step would undershoot, or difference == 0).-30 < difference < 0 after step 2/3, subtract |difference| from the next entry in the walk order that can absorb it without dropping below 30 minutes. This is the ONLY non-30 adjustment allowed and brings difference to exactly 0. If no entry can absorb the residual without floor violation, walk forward seeking one that can. Print: 🔧 Residual trim: AB-1234 -15m to close 8h gap.🔧 Adjusted AB-1234 "code changes" from 4h 0m → 4h 30m to fill 8h day.If existing_total + meeting_total already exceeds 480 minutes, do not shrink user entries below their original size to compensate - print a warning that the day is already over 8h before any user input and proceed with the user's original durations.
This adjustment MUST happen before scheduling (Step 8) so the time windows are calculated correctly.
Build the day's schedule starting at 09:00 (9 AM).
Now POST every record from the plan - newly planned meetings (Step 5, post-dedup) AND user work entries (Step 8). Never re-POST existing records from Step 4b - they already exist and a re-POST would create duplicates.
Tempo has no bulk endpoint, so fire every record concurrently as a single batch and wait once. Capture each response to a separate file/array keyed by record index so per-record success can be aggregated after wait.
for i in "${!ALL_RECORDS[@]}"; do
curl -s -X POST "https://api.tempo.io/4/worklogs" \
-H "Authorization: Bearer ${TEMPO_API_TOKEN}" \
-H "Content-Type: application/json" \
-d "${ALL_RECORDS[$i]}" \
> "/tmp/resp_${i}.json" &
done
wait
Do not wrap curl calls in a serial loop (e.g. Python subprocess.run([...]) without async/threading) - that defeats the parallelism and makes a 10-record day take 10× longer than a 1-record day. Use asyncio + aiohttp or concurrent.futures.ThreadPoolExecutor(max_workers=16) if implementing in Python.
If a POST returns a non-2xx response, capture and print the response body. After wait, if any POST failed, print every failure and exit non-zero - do not retry, do not silently skip.
Print each posted entry as confirmation:
✅ <HH:MM>-<HH:MM> | <TICKET> | <duration> | <description>
Sum all logged time on ${TARGET_DATE} in minutes - that means existing records (Step 4b) plus every record this skill just posted in Step 9.
✅ Day complete - 8h logged.⚠️ Day incomplete - missing <X>h <Y>m. Add more entries to fill the day.⚠️ Day exceeds 8h - total is <X>h <Y>m. Review entries.Print a table of ALL records for the day - both existing (from Step 4b) and newly posted (from Step 9). Mark each row so the user can tell them apart (e.g. trailing (existing) vs (NEW)):
📋 Tempo Log for ${TARGET_DATE}
─────────────────────────────────────────────
Time │ Ticket │ Duration │ Description
09:00-10:00 │ AB-1234 │ 1h 0m │ coding
10:00-10:30 │ AB-9999 │ 0h 30m │ PR review
10:30-11:30 │ AB-1234 │ 1h 0m │ Team Daily
11:30-14:30 │ AB-5555 │ 3h 0m │ Work on AB-5555
...
─────────────────────────────────────────────
Total: Xh Ym / 8h 0m
${TEMPO_MEETING_TICKET} - but skip any meeting already covered by an existing meeting worklog on ${TARGET_DATE} (dedup against Step 4b).generate QA test cases from a jira ticket, github pr, free text, conversation, or any mix of context sources.
analyze a feature context (PR, Jira ticket, Confluence page, URL, free text, or current conversation) and produce AC bullet points for QA.
Backfill missing Tempo meeting worklogs from a user-provided start date.
generate a jira bug ticket title and description from the current conversation.
deep, harsh code review of a github pull request by number or branch name.
fetch a jira ticket and produce a detailed implementation guide based on the current codebase.