Skip to main content
Glama

bulk_post_worklogs

Create multiple worklog entries in JIRA Tempo from structured data to log time across multiple issues and dates efficiently.

Instructions

Create multiple worklog entries from a structured format. RECOMMENDED: Use get_schedule first to identify working days and avoid logging time on non-working days.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
worklogsYesArray of worklog entries to create
billableNoWhether the time is billable for all entries (default: true)

Implementation Reference

  • Core handler function for bulk_post_worklogs tool. Converts input to Tempo API format, calls createWorklogsBatch concurrently, processes results, generates summary statistics, daily totals pivot table in markdown, and returns structured response with error handling.
    export async function bulkPostWorklogs( tempoClient: TempoClient, input: BulkPostWorklogsInput ): Promise<CallToolResult> { try { const { worklogs, billable = true } = input; if (worklogs.length === 0) { return { content: [ { type: "text", text: "No worklog entries provided." } ], isError: true }; } // Validate maximum entries (prevent overwhelming the API) if (worklogs.length > 100) { return { content: [ { type: "text", text: "Too many worklog entries. Maximum 100 entries allowed per bulk operation." } ], isError: true }; } // Convert bulk entries to the format expected by the Tempo client (worker auto-determined) const worklogParams = worklogs.map((entry: BulkWorklogEntry) => ({ issueKey: entry.issueKey, hours: entry.hours, startDate: entry.date, endDate: entry.date, // Single day entries billable, description: entry.description })); let displayText = `## Bulk Worklog Creation Started\n\n`; displayText += `Processing ${worklogs.length} worklog entries...\n\n`; // Use the Tempo client's batch creation method (implements Promise.all internally) const results = await tempoClient.createWorklogsBatch(worklogParams); // Analyze results const successful = results.filter(r => r.success); const failed = results.filter(r => !r.success); const totalHours = successful.reduce((sum, result) => { return sum + result.originalParams.hours; }, 0); // Build response object const response: BulkPostWorklogsResponse = { results: results.map(result => ({ success: result.success, worklog: result.worklog ? { id: result.worklog.id || 'unknown', issueKey: result.worklog.issue.key, issueSummary: result.worklog.issue.summary, timeSpentSeconds: result.worklog.timeSpentSeconds, billableSeconds: result.worklog.billableSeconds, started: result.worklog.started, worker: result.worklog.worker, attributes: result.worklog.attributes || {}, timeSpent: result.worklog.timeSpent } : undefined, error: result.error, issueKey: result.originalParams.issueKey, date: result.originalParams.startDate, hours: result.originalParams.hours })), summary: { totalEntries: worklogs.length, successful: successful.length, failed: failed.length, totalHours }, dailyTotals: {} }; // Calculate daily totals by issue (matching the C# pivot table pattern) const dailyTotals: Record<string, Record<string, number>> = {}; successful.forEach(result => { const date = result.originalParams.startDate; const issueKey = result.originalParams.issueKey; const hours = result.originalParams.hours; if (!dailyTotals[date]) { dailyTotals[date] = {}; } if (!dailyTotals[date][issueKey]) { dailyTotals[date][issueKey] = 0; } dailyTotals[date][issueKey] += hours; }); response.dailyTotals = dailyTotals; // Format display text displayText += `## Results Summary\n\n`; displayText += `- **Total Entries:** ${response.summary.totalEntries}\n`; displayText += `- **Successful:** ${response.summary.successful}\n`; displayText += `- **Failed:** ${response.summary.failed}\n`; displayText += `- **Total Hours:** ${response.summary.totalHours}\n\n`; if (successful.length > 0) { displayText += `### βœ… Successful Entries (${successful.length})\n\n`; // Group by date for better readability const successByDate = successful.reduce((acc, result) => { const date = result.originalParams.startDate; if (!acc[date]) { acc[date] = []; } acc[date].push(result); return acc; }, {} as Record<string, typeof successful>); const sortedDates = Object.keys(successByDate).sort(); for (const date of sortedDates) { const dayEntries = successByDate[date]; const dayHours = dayEntries.reduce((sum, entry) => sum + entry.originalParams.hours, 0); displayText += `#### ${date} (${dayHours}h total)\n\n`; for (const result of dayEntries) { const { issueKey, hours, description } = result.originalParams; displayText += `- **${issueKey}**: ${hours}h`; if (result.worklog) { displayText += ` - ${result.worklog.issue.summary}`; } if (description) { displayText += `\n *${description}*`; } displayText += `\n`; } displayText += `\n`; } } if (failed.length > 0) { displayText += `### ❌ Failed Entries (${failed.length})\n\n`; for (const result of failed) { const { issueKey, hours, startDate, description } = result.originalParams; displayText += `- **${issueKey}** (${startDate}, ${hours}h)`; if (description) { displayText += ` - *${description}*`; } displayText += `\n **Error:** ${result.error}\n\n`; } } // Add daily totals table (matching C# pivot table format) if (Object.keys(dailyTotals).length > 0) { displayText += `### πŸ“Š Daily Totals by Issue\n\n`; const allIssues = new Set<string>(); Object.values(dailyTotals).forEach(dayData => { Object.keys(dayData).forEach(issue => allIssues.add(issue)); }); const sortedIssues = Array.from(allIssues).sort(); const sortedDailyDates = Object.keys(dailyTotals).sort(); // Create table header displayText += `| Date | ${sortedIssues.join(' | ')} | Total |\n`; displayText += `|------|${sortedIssues.map(() => '---').join('|')}|-------|\n`; // Add rows for each date for (const date of sortedDailyDates) { const dayData = dailyTotals[date]; const rowValues = sortedIssues.map(issue => { const hours = dayData[issue] || 0; return hours > 0 ? `${hours}h` : '-'; }); const dayTotal = Object.values(dayData).reduce((sum, hours) => sum + hours, 0); displayText += `| ${date} | ${rowValues.join(' | ')} | **${dayTotal}h** |\n`; } // Add totals row const issueTotals = sortedIssues.map(issue => { const total = sortedDailyDates.reduce((sum, date) => { return sum + (dailyTotals[date][issue] || 0); }, 0); return total > 0 ? `**${total}h**` : '-'; }); displayText += `| **Total** | ${issueTotals.join(' | ')} | **${response.summary.totalHours}h** |\n`; } return { content: [ { type: "text", text: displayText } ], isError: failed.length === worklogs.length // Only error if ALL failed }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `## Error in Bulk Worklog Creation\n\n**Error:** ${errorMessage}\n\n**Entries to process:** ${input.worklogs.length}` } ], isError: true }; }
  • src/index.ts:134-176 (registration)
    Tool registration in MCP ListTools handler defining the tool name, description, and JSON input schema for discovery.
    { name: TOOL_NAMES.BULK_POST_WORKLOGS, description: "Create multiple worklog entries from a structured format. RECOMMENDED: Use get_schedule first to identify working days and avoid logging time on non-working days.", inputSchema: { type: "object", properties: { worklogs: { type: "array", items: { type: "object", properties: { issueKey: { type: "string", description: "JIRA issue key (e.g., PROJ-1234)", }, hours: { type: "number", minimum: 0.1, maximum: 24, description: "Hours worked (decimal)", }, date: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$", description: "Date in YYYY-MM-DD format", }, description: { type: "string", description: "Work description (optional)", }, }, required: ["issueKey", "hours", "date"], }, description: "Array of worklog entries to create", }, billable: { type: "boolean", description: "Whether the time is billable for all entries (default: true)", }, }, required: ["worklogs"], }, },
  • Zod schemas for runtime input validation of bulk_post_worklogs tool arguments, including individual entry validation.
    export const BulkWorklogEntrySchema = z.object({ issueKey: z.string().min(1, "Issue key is required"), hours: z.number().min(0.1, "Hours must be at least 0.1").max(24, "Hours cannot exceed 24"), date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format"), description: z.string().optional(), }); // Bulk post worklogs tool input schema export const BulkPostWorklogsInputSchema = z.object({ worklogs: z.array(BulkWorklogEntrySchema).min(1, "At least one worklog entry is required"), billable: z.boolean().optional(), });
  • src/index.ts:231-234 (registration)
    Dispatch handler in MCP CallToolRequest that parses input using Zod schema and invokes the bulkPostWorklogs implementation.
    case TOOL_NAMES.BULK_POST_WORKLOGS: { const input = BulkPostWorklogsInputSchema.parse(args); return await bulkPostWorklogs(tempoClient, input); }
  • Re-export of the bulkPostWorklogs handler for use in src/index.ts.
    export { bulkPostWorklogs } from "./bulk-post.js";

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/TRANZACT/tempo-filler-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server