autotask_create_time_entry
Log work hours to Autotask tickets, projects, or regular time categories (meetings, admin) with date and notes.
Instructions
Create a time entry in Autotask. Can be tied to a ticket, task, or project, OR created as "Regular Time" (no parent) for meetings, admin work, etc. For Regular Time, specify a category like "Internal Meeting", "Office Management", "Training", etc.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| ticketID | No | Ticket ID for the time entry (omit for Regular Time) | |
| taskID | No | Task ID for the time entry (for project work, omit for Regular Time) | |
| projectID | No | Project ID for the time entry (omit for Regular Time) | |
| resourceID | No | Resource ID (user) logging the time. Can be omitted if resourceName is provided. | |
| resourceName | No | Name of the resource/user (e.g., "Will Spence"). Will be resolved to a resourceID automatically. Use this instead of resourceID for convenience. | |
| category | No | Category name for Regular Time entries (e.g., "Internal Meeting", "Office Management", "Training", "Research", "HR/Recruiting", "Travel Time", "Holiday", "PTO"). Required for Regular Time entries (when no ticket/task/project is specified). | |
| dateWorked | Yes | Date worked (YYYY-MM-DD format) | |
| startDateTime | No | Start date/time (ISO format) | |
| endDateTime | No | End date/time (ISO format) | |
| hoursWorked | Yes | Number of hours worked | |
| summaryNotes | Yes | Summary notes for the time entry | |
| internalNotes | No | Internal notes for the time entry |
Implementation Reference
- src/handlers/tool.handler.ts:924-957 (handler)Handler function dispatched for 'autotask_create_time_entry'. Resolves resource by name if resourceName is provided, handles Regular Time category selection, then calls s.createTimeEntry(a).
['autotask_create_time_entry', async (a) => { // If no resource specified at all, prompt the user if (!a.resourceID && !a.resourceName) { return { result: null, message: 'Please specify who is logging this time. Provide a resourceName (e.g., "Will Spence") or resourceID.' }; } // Resolve resourceName to resourceID via SDK helper if (a.resourceName && !a.resourceID) { const resource = await s.resolveResourceByName(a.resourceName); if (!resource) { throw new Error(`No resource found matching "${a.resourceName}"`); } a.resourceID = resource.id; delete a.resourceName; } // For Regular Time entries (no ticket/task/project), handle category const isRegularTime = !a.ticketID && !a.taskID && !a.projectID; if (isRegularTime) { if (!a.category && !a.internalBillingCodeID) { // List available categories and prompt user const categories = await s.getInternalBillingCodeNames(); return { result: null, message: `Please specify a category for this Regular Time entry. Available categories: ${categories.join(', ')}` }; } if (a.category && !a.internalBillingCodeID) { const billingCode = await s.resolveInternalBillingCodeByName(a.category); if (!billingCode) { const categories = await s.getInternalBillingCodeNames(); throw new Error(`No category found matching "${a.category}". Available categories: ${categories.join(', ')}`); } a.internalBillingCodeID = billingCode.id; delete a.category; } } const id = await s.createTimeEntry(a); return { result: id, message: `Successfully created time entry with ID: ${id}` }; }], - Input schema definition for autotask_create_time_entry tool, defining parameters like ticketID, taskID, projectID, resourceID, resourceName, category, dateWorked, hoursWorked, summaryNotes, etc.
{ name: 'autotask_create_time_entry', description: 'Create a time entry in Autotask. Can be tied to a ticket, task, or project, OR created as "Regular Time" (no parent) for meetings, admin work, etc. For Regular Time, specify a category like "Internal Meeting", "Office Management", "Training", etc.', inputSchema: { type: 'object', properties: { ticketID: { type: 'number', description: 'Ticket ID for the time entry (omit for Regular Time)' }, taskID: { type: 'number', description: 'Task ID for the time entry (for project work, omit for Regular Time)' }, projectID: { type: 'number', description: 'Project ID for the time entry (omit for Regular Time)' }, resourceID: { type: 'number', description: 'Resource ID (user) logging the time. Can be omitted if resourceName is provided.' }, resourceName: { type: 'string', description: 'Name of the resource/user (e.g., "Will Spence"). Will be resolved to a resourceID automatically. Use this instead of resourceID for convenience.' }, category: { type: 'string', description: 'Category name for Regular Time entries (e.g., "Internal Meeting", "Office Management", "Training", "Research", "HR/Recruiting", "Travel Time", "Holiday", "PTO"). Required for Regular Time entries (when no ticket/task/project is specified).' }, dateWorked: { type: 'string', description: 'Date worked (YYYY-MM-DD format)' }, startDateTime: { type: 'string', description: 'Start date/time (ISO format)' }, endDateTime: { type: 'string', description: 'End date/time (ISO format)' }, hoursWorked: { type: 'number', description: 'Number of hours worked' }, summaryNotes: { type: 'string', description: 'Summary notes for the time entry' }, internalNotes: { type: 'string', description: 'Internal notes for the time entry' } }, required: ['dateWorked', 'hoursWorked', 'summaryNotes'] } - src/handlers/tool.handler.ts:764-1406 (registration)Registration of autotask_create_time_entry in the dispatch table used by callTool() method.
private getDispatchTable(): Map<string, (args: any) => Promise<{ result: any; message: string }>> { const s = this.autotaskService; type H = (args: any) => Promise<{ result: any; message: string }>; return new Map<string, H>([ // Connection ['autotask_test_connection', async () => { const ok = await s.testConnection(); if (!ok) { throw new Error('Connection to Autotask API failed. Verify AUTOTASK_USERNAME, AUTOTASK_SECRET, and AUTOTASK_INTEGRATION_CODE are configured correctly and that the API user has at least read access to Companies.'); } return { result: { success: true }, message: 'Successfully connected to Autotask API' }; }], // Companies ['autotask_search_companies', async (a) => { const r = await s.searchCompanies(a); return { result: r, message: `Found ${r.length} companies` }; }], ['autotask_create_company', async (a) => { const id = await s.createCompany(a); return { result: id, message: `Successfully created company with ID: ${id}` }; }], ['autotask_update_company', async (a) => { await s.updateCompany(a.id, a); return { result: undefined, message: `Successfully updated company ID: ${a.id}` }; }], ['autotask_get_company_site_configuration', async (a) => { const r = await s.getCompanySiteConfigurations(a.companyId); return { result: r, message: `Found ${r.length} site configuration record(s) for company ${a.companyId}` }; }], ['autotask_update_company_site_configuration', async (a) => { await s.updateCompanySiteConfiguration(a.id, a.updates || {}); return { result: undefined, message: `Successfully updated company site configuration ID: ${a.id}` }; }], // Contacts ['autotask_search_contacts', async (a) => { const r = await s.searchContacts(a); return { result: r, message: `Found ${r.length} contacts` }; }], ['autotask_create_contact', async (a) => { const id = await s.createContact(a); return { result: id, message: `Successfully created contact with ID: ${id}` }; }], ['autotask_update_contact', async (a) => { await s.updateContact(a.id, a); return { result: undefined, message: `Successfully updated contact ID: ${a.id}` }; }], // Tickets ['autotask_search_tickets', async (a) => { // Elicitation for zero-filter ticket searches const hasFilters = a.searchTerm || a.companyID || a.status !== undefined || a.assignedResourceID || a.unassigned || a.createdAfter || a.createdBefore || a.lastActivityAfter; if (!hasFilters && this.mcpServer) { const dateChoice = await this.elicitDateRange(); if (dateChoice) a = { ...a, ...dateChoice }; } const { companyID, ...rest } = a; const opts = { ...rest, ...(companyID !== undefined && { companyId: companyID }) }; const r = await s.searchTickets(opts); return { result: r, message: `Found ${r.length} tickets` }; }], ['autotask_get_ticket_details', async (a) => { const r = await s.getTicket(a.ticketID, a.fullDetails); return { result: r, message: 'Ticket details retrieved successfully' }; }], ['autotask_create_ticket', async (a) => { const payload = buildTicketPayload(a); const id = await s.createTicket(payload); return { result: id, message: `Successfully created ticket with ID: ${id}` }; }], ['autotask_update_ticket', async (a) => { const { ticketId, ...rest } = a; const payload = buildTicketPayload(rest); await s.updateTicket(ticketId, payload); return { result: ticketId, message: `Successfully updated ticket ${ticketId}` }; }], // Ticket Charges ['autotask_get_ticket_charge', async (a) => { const r = await s.getTicketCharge(a.chargeId); if (!r) return { result: null, message: `No ticket charge found with ID ${a.chargeId}` }; return { result: r, message: 'Ticket charge retrieved successfully' }; }], ['autotask_search_ticket_charges', async (a) => { const r = await s.searchTicketCharges(a); return { result: r, message: `Found ${r.length} ticket charges` }; }], ['autotask_create_ticket_charge', async (a) => { const id = await s.createTicketCharge(a); return { result: id, message: `Successfully created ticket charge with ID: ${id}` }; }], ['autotask_update_ticket_charge', async (a) => { const { chargeId, ...updates } = a; await s.updateTicketCharge(chargeId, updates); return { result: chargeId, message: `Successfully updated ticket charge ${chargeId}` }; }], ['autotask_delete_ticket_charge', async (a) => { await s.deleteTicketCharge(a.ticketId, a.chargeId); return { result: a.chargeId, message: `Successfully deleted ticket charge ${a.chargeId}` }; }], // Ticket History (read-only audit trail) ['autotask_get_ticket_history', async (a) => { const r = await s.getTicketHistory(a.historyId); if (!r) return { result: null, message: `No ticket history entry found with ID ${a.historyId}` }; return { result: r, message: 'Ticket history entry retrieved successfully' }; }], ['autotask_search_ticket_history', async (a) => { const r = await s.searchTicketHistory({ ticketId: a.ticketId, pageSize: a.pageSize }); return { result: r, message: `Found ${r.length} ticket history entries for ticket ${a.ticketId}` }; }], // Service Calls ['autotask_get_service_call', async (a) => { const r = await s.getServiceCall(a.serviceCallId); if (!r) return { result: null, message: `No service call found with ID ${a.serviceCallId}` }; return { result: r, message: 'Service call retrieved successfully' }; }], ['autotask_search_service_calls', async (a) => { const r = await s.searchServiceCalls(a); return { result: r, message: `Found ${r.length} service calls` }; }], ['autotask_create_service_call', async (a) => { const id = await s.createServiceCall(a); return { result: id, message: `Successfully created service call with ID: ${id}` }; }], ['autotask_update_service_call', async (a) => { const { serviceCallId, ...updates } = a; await s.updateServiceCall(serviceCallId, updates); return { result: serviceCallId, message: `Successfully updated service call ${serviceCallId}` }; }], ['autotask_delete_service_call', async (a) => { await s.deleteServiceCall(a.serviceCallId); return { result: a.serviceCallId, message: `Successfully deleted service call ${a.serviceCallId}` }; }], // ServiceCallTickets ['autotask_search_service_call_tickets', async (a) => { const r = await s.searchServiceCallTickets(a); return { result: r, message: `Found ${r.length} service call tickets` }; }], ['autotask_create_service_call_ticket', async (a) => { const id = await s.createServiceCallTicket(a); return { result: id, message: `Successfully linked ticket to service call, record ID: ${id}` }; }], ['autotask_delete_service_call_ticket', async (a) => { await s.deleteServiceCallTicket(a.serviceCallTicketId); return { result: a.serviceCallTicketId, message: `Successfully removed ticket from service call` }; }], // ServiceCallTicketResources ['autotask_search_service_call_ticket_resources', async (a) => { const r = await s.searchServiceCallTicketResources(a); return { result: r, message: `Found ${r.length} service call ticket resources` }; }], ['autotask_create_service_call_ticket_resource', async (a) => { const id = await s.createServiceCallTicketResource(a); return { result: id, message: `Successfully assigned resource to service call ticket, record ID: ${id}` }; }], ['autotask_delete_service_call_ticket_resource', async (a) => { await s.deleteServiceCallTicketResource(a.serviceCallTicketResourceId); return { result: a.serviceCallTicketResourceId, message: `Successfully removed resource from service call ticket` }; }], // Time entries ['autotask_create_time_entry', async (a) => { // If no resource specified at all, prompt the user if (!a.resourceID && !a.resourceName) { return { result: null, message: 'Please specify who is logging this time. Provide a resourceName (e.g., "Will Spence") or resourceID.' }; } // Resolve resourceName to resourceID via SDK helper if (a.resourceName && !a.resourceID) { const resource = await s.resolveResourceByName(a.resourceName); if (!resource) { throw new Error(`No resource found matching "${a.resourceName}"`); } a.resourceID = resource.id; delete a.resourceName; } // For Regular Time entries (no ticket/task/project), handle category const isRegularTime = !a.ticketID && !a.taskID && !a.projectID; if (isRegularTime) { if (!a.category && !a.internalBillingCodeID) { // List available categories and prompt user const categories = await s.getInternalBillingCodeNames(); return { result: null, message: `Please specify a category for this Regular Time entry. Available categories: ${categories.join(', ')}` }; } if (a.category && !a.internalBillingCodeID) { const billingCode = await s.resolveInternalBillingCodeByName(a.category); if (!billingCode) { const categories = await s.getInternalBillingCodeNames(); throw new Error(`No category found matching "${a.category}". Available categories: ${categories.join(', ')}`); } a.internalBillingCodeID = billingCode.id; delete a.category; } } const id = await s.createTimeEntry(a); return { result: id, message: `Successfully created time entry with ID: ${id}` }; }], // Projects ['autotask_search_projects', async (a) => { const r = await s.searchProjects(a); return { result: r, message: `Found ${r.length} projects` }; }], ['autotask_create_project', async (a) => { const projectData = { ...a }; // Map startDate/endDate (YYYY-MM-DD) to startDateTime/endDateTime (ISO) expected by the API if (projectData.startDate && !projectData.startDateTime) { projectData.startDateTime = `${projectData.startDate}T00:00:00Z`; delete projectData.startDate; } if (projectData.endDate && !projectData.endDateTime) { projectData.endDateTime = `${projectData.endDate}T00:00:00Z`; delete projectData.endDate; } const id = await s.createProject(projectData); return { result: id, message: `Successfully created project with ID: ${id}` }; }], ['autotask_update_project', async (a) => { const { projectId, ...rest } = a; const updates: Record<string, any> = {}; for (const key of [ 'projectName', 'description', 'status', 'departmentID', 'assignedResourceID', 'assignedResourceRoleID', 'projectLeadResourceID', 'startDateTime', 'endDateTime', 'estimatedTime', 'userDefinedFields' ]) { if (rest[key] !== undefined) updates[key] = rest[key]; } await s.updateProject(projectId, updates); return { result: undefined, message: `Successfully updated project ID: ${projectId}` }; }], // Resources ['autotask_search_resources', async (a) => { const r = await s.searchResources(a); return { result: r, message: `Found ${r.length} resources` }; }], // Configuration Items ['autotask_search_configuration_items', async (a) => { const r = await s.searchConfigurationItems(a); return { result: r, message: `Found ${r.length} configuration items` }; }], // Contracts ['autotask_search_contracts', async (a) => { const r = await s.searchContracts(a); return { result: r, message: `Found ${r.length} contracts` }; }], ['autotask_create_contract', async (a) => { const id = await s.createContract(a); return { result: id, message: `Successfully created contract with ID: ${id}` }; }], ['autotask_update_contract', async (a) => { const { id, ...rest } = a; await s.updateContract(id, rest); return { result: undefined, message: `Successfully updated contract ID: ${id}` }; }], ['autotask_create_contract_service', async (a) => { const id = await s.createContractService(a); return { result: id, message: `Successfully created contract service with ID: ${id}` }; }], ['autotask_update_contract_service', async (a) => { const { id, ...rest } = a; await s.updateContractService(id, rest); return { result: undefined, message: `Successfully updated contract service ID: ${id}` }; }], // Raw REST passthrough (escape hatch) ['autotask_raw_request', async (a) => { const r = await s.rawRequest(a.method, a.path, a.body, a.queryParams); return { result: r, message: `Autotask ${a.method} ${a.path} completed` }; }], // Invoices ['autotask_search_invoices', async (a) => { const r = await s.searchInvoices(a); return { result: r, message: `Found ${r.length} invoices` }; }], ['autotask_get_invoice_details', async (a) => { const r = await s.getInvoiceDetails(a.invoiceId); const count = r?.lineItems?.length ?? 0; return { result: r, message: r ? `Invoice ${a.invoiceId} retrieved with ${count} line items` : `Invoice ${a.invoiceId} not found` }; }], // Tasks ['autotask_search_tasks', async (a) => { const r = await s.searchTasks(a); return { result: r, message: `Found ${r.length} tasks` }; }], ['autotask_create_task', async (a) => { const taskData = { ...a, taskType: a.taskType ?? 1 }; const id = await s.createTask(taskData); return { result: id, message: `Successfully created task with ID: ${id}` }; }], // Phases ['autotask_list_phases', async (a) => { const r = await s.searchPhases(a.projectID, { pageSize: a.pageSize }); return { result: r, message: `Found ${r.length} phases` }; }], ['autotask_create_phase', async (a) => { const id = await s.createPhase(a); return { result: id, message: `Successfully created phase with ID: ${id}` }; }], // Notes (ticket/project/company) ['autotask_get_ticket_note', async (a) => { const r = await s.getTicketNote(a.ticketId, a.noteId); return { result: r, message: 'Ticket note retrieved successfully' }; }], ['autotask_search_ticket_notes', async (a) => { const r = await s.searchTicketNotes(a.ticketId, { pageSize: a.pageSize }); return { result: r, message: `Found ${r.length} ticket notes` }; }], ['autotask_create_ticket_note', async (a) => { const id = await s.createTicketNote(a.ticketId, { title: a.title || 'Note', description: a.description, noteType: a.noteType ?? 1, publish: a.publish ?? 1 }); return { result: id, message: `Successfully created ticket note with ID: ${id}` }; }], // Ticket Checklist Items ['autotask_search_ticket_checklist_items', async (a) => { const r = await s.searchTicketChecklistItems(a.ticketId); return { result: r, message: `Found ${r.length} checklist items` }; }], ['autotask_create_ticket_checklist_item', async (a) => { const id = await s.createTicketChecklistItem(a.ticketId, { itemName: a.itemName, position: a.position, isCompleted: a.isCompleted }); return { result: id, message: `Successfully created ticket checklist item with ID: ${id}` }; }], ['autotask_update_ticket_checklist_item', async (a) => { await s.updateTicketChecklistItem(a.ticketId, a.itemId, { itemName: a.itemName, isCompleted: a.isCompleted, position: a.position }); return { result: a.itemId, message: `Successfully updated ticket checklist item ${a.itemId}` }; }], ['autotask_delete_ticket_checklist_item', async (a) => { await s.deleteTicketChecklistItem(a.ticketId, a.itemId); return { result: a.itemId, message: `Successfully deleted ticket checklist item ${a.itemId}` }; }], ['autotask_get_project_note', async (a) => { const r = await s.getProjectNote(a.projectId, a.noteId); return { result: r, message: 'Project note retrieved successfully' }; }], ['autotask_search_project_notes', async (a) => { const r = await s.searchProjectNotes(a.projectId, { pageSize: a.pageSize }); return { result: r, message: `Found ${r.length} project notes` }; }], ['autotask_create_project_note', async (a) => { const id = await s.createProjectNote(a.projectId, { title: a.title, description: a.description, noteType: a.noteType, publish: a.publish ?? 1, isAnnouncement: a.isAnnouncement ?? false }); return { result: id, message: `Successfully created project note with ID: ${id}` }; }], ['autotask_get_company_note', async (a) => { const r = await s.getCompanyNote(a.companyId, a.noteId); return { result: r, message: 'Company note retrieved successfully' }; }], ['autotask_search_company_notes', async (a) => { const r = await s.searchCompanyNotes(a.companyId, { pageSize: a.pageSize }); return { result: r, message: `Found ${r.length} company notes` }; }], ['autotask_create_company_note', async (a) => { const id = await s.createCompanyNote(a.companyId, { title: a.title, description: a.description, actionType: a.actionType }); return { result: id, message: `Successfully created company note with ID: ${id}` }; }], // Attachments ['autotask_get_ticket_attachment', async (a) => { const r = await s.getTicketAttachment(a.ticketId, a.attachmentId, { includeData: a.includeData, maxInlineBase64Bytes: a.maxInlineBase64Bytes, }); if (!r) return { result: null, message: `No ticket attachment found with ID ${a.attachmentId} on ticket ${a.ticketId}` }; const message = r.dataOmittedReason ? `Ticket attachment retrieved (data omitted: oversized for inline transport)` : 'Ticket attachment retrieved successfully'; return { result: r, message }; }], ['autotask_search_ticket_attachments', async (a) => { const r = await s.searchTicketAttachments(a.ticketId, { pageSize: a.pageSize }); return { result: r, message: `Found ${r.length} ticket attachments` }; }], ['autotask_create_ticket_attachment', async (a) => { // Never log `data` (base64 file bytes) — can be large / contain PII. const decodedBytes = typeof a.data === 'string' ? Buffer.from(a.data, 'base64').length : 0; this.logger.info( `autotask_create_ticket_attachment invoked: ticketId=${a.ticketId} title="${a.title}" bytes=${decodedBytes}` ); const id = await s.createTicketAttachment(a.ticketId, { title: a.title, fullPath: a.fullPath || a.title, data: a.data, contentType: a.contentType, publish: a.publish ?? 1 }); return { result: id, message: `Successfully created ticket attachment with ID: ${id}` }; }], // Expense Reports ['autotask_get_expense_report', async (a) => { const r = await s.getExpenseReport(a.reportId); return { result: r, message: 'Expense report retrieved successfully' }; }], ['autotask_search_expense_reports', async (a) => { const r = await s.searchExpenseReports({ submitterId: a.submitterId, status: a.status, pageSize: a.pageSize }); return { result: r, message: `Found ${r.length} expense reports` }; }], ['autotask_create_expense_report', async (a) => { const id = await s.createExpenseReport({ name: a.name, description: a.description, submitterID: a.submitterId, weekEnding: a.weekEndingDate || a.weekEnding }); return { result: id, message: `Successfully created expense report with ID: ${id}` }; }], // Expense Items ['autotask_create_expense_item', async (a) => { const id = await s.createExpenseItem({ expenseReportID: a.expenseReportId, description: a.description, expenseDate: a.expenseDate, expenseCategory: a.expenseCategory, expenseCurrencyExpenseAmount: a.amount, companyID: a.companyId ?? 0, haveReceipt: a.haveReceipt ?? false, isBillableToCompany: a.isBillableToCompany ?? false, isReimbursable: a.isReimbursable ?? true, paymentType: a.paymentType ?? 10 }); return { result: id, message: `Successfully created expense item with ID: ${id}` }; }], // Quotes ['autotask_get_quote', async (a) => { const r = await s.getQuote(a.quoteId); return { result: r, message: 'Quote retrieved successfully' }; }], ['autotask_search_quotes', async (a) => { const r = await s.searchQuotes({ companyId: a.companyId, contactId: a.contactId, opportunityId: a.opportunityId, searchTerm: a.searchTerm, pageSize: a.pageSize }); return { result: r, message: `Found ${r.length} quotes` }; }], ['autotask_create_quote', async (a) => { // Elicit company if not provided if (!a.companyId && this.mcpServer) { try { const companyId = await this.elicitCompanyId(); if (companyId) a = { ...a, companyId: companyId }; } catch { /* proceed without company */ } } // Elicit opportunity if not provided but company is known if (!a.opportunityId && a.companyId && this.mcpServer) { try { const opps = await s.searchOpportunities({ companyId: a.companyId }); if (opps.length > 0) { const options: PicklistValue[] = opps .filter(o => o.id != null) .map(o => ({ value: String(o.id), label: o.title || `Opportunity #${o.id}`, })); const selected = await this.elicitSelection( `Found ${opps.length} opportunities for this company. Which one should the quote be attached to?`, 'opportunityId', options ); if (selected) a = { ...a, opportunityId: Number(selected) }; } } catch { /* proceed without opportunity */ } } const id = await s.createQuote({ name: a.name, description: a.description, companyID: a.companyId, contactID: a.contactId, opportunityID: a.opportunityId, effectiveDate: a.effectiveDate, expirationDate: a.expirationDate }); return { result: id, message: `Successfully created quote with ID: ${id}` }; }], // Opportunities ['autotask_get_opportunity', async (a) => { const r = await s.getOpportunity(a.opportunityId); return { result: r, message: 'Opportunity retrieved successfully' }; }], ['autotask_search_opportunities', async (a) => { const r = await s.searchOpportunities({ companyId: a.companyId, searchTerm: a.searchTerm, status: a.status, pageSize: a.pageSize }); return { result: r, message: `Found ${r.length} opportunities` }; }], ['autotask_create_opportunity', async (a) => { const id = await s.createOpportunity({ title: a.title, companyID: a.companyId, ownerResourceID: a.ownerResourceId, status: a.status, stage: a.stage, projectedCloseDate: a.projectedCloseDate, startDate: a.startDate, probability: a.probability ?? 50, amount: a.amount ?? 0, cost: a.cost ?? 0, useQuoteTotals: a.useQuoteTotals ?? true, totalAmountMonths: a.totalAmountMonths, contactID: a.contactId, description: a.description, opportunityCategoryID: a.opportunityCategoryID }); return { result: id, message: `Successfully created opportunity with ID: ${id}` }; }], // Products ['autotask_get_product', async (a) => { const r = await s.getProduct(a.productId); return { result: r, message: 'Product retrieved successfully' }; }], ['autotask_search_products', async (a) => { const r = await s.searchProducts({ searchTerm: a.searchTerm, isActive: a.isActive, pageSize: a.pageSize }); return { result: r, message: `Found ${r.length} products` }; }], // Services ['autotask_get_service', async (a) => { const r = await s.getService(a.serviceId); return { result: r, message: 'Service retrieved successfully' }; }], ['autotask_search_services', async (a) => { const r = await s.searchServices({ searchTerm: a.searchTerm, isActive: a.isActive, pageSize: a.pageSize }); return { result: r, message: `Found ${r.length} services` }; }], // Service Bundles ['autotask_get_service_bundle', async (a) => { const r = await s.getServiceBundle(a.serviceBundleId); return { result: r, message: 'Service bundle retrieved successfully' }; }], ['autotask_search_service_bundles', async (a) => { const r = await s.searchServiceBundles({ searchTerm: a.searchTerm, isActive: a.isActive, pageSize: a.pageSize }); return { result: r, message: `Found ${r.length} service bundles` }; }], // Quote Items ['autotask_get_quote_item', async (a) => { const r = await s.getQuoteItem(a.quoteItemId); return { result: r, message: 'Quote item retrieved successfully' }; }], ['autotask_search_quote_items', async (a) => { const r = await s.searchQuoteItems({ quoteId: a.quoteId, searchTerm: a.searchTerm, pageSize: a.pageSize }); return { result: r, message: `Found ${r.length} quote items` }; }], ['autotask_create_quote_item', async (a) => { // Elicit service/product selection when no ID is provided but name is available if (!a.serviceID && !a.productID && !a.serviceBundleID && a.name && this.mcpServer) { try { const itemChoice = await this.elicitItemSelection(a.name); if (itemChoice) a = { ...a, ...itemChoice }; } catch { /* proceed as cost-type item */ } } const id = await s.createQuoteItem({ quoteID: a.quoteId, name: a.name, description: a.description, quantity: a.quantity, unitPrice: a.unitPrice, unitCost: a.unitCost, unitDiscount: a.unitDiscount, lineDiscount: a.lineDiscount, percentageDiscount: a.percentageDiscount, isOptional: a.isOptional, serviceID: a.serviceID, productID: a.productID, serviceBundleID: a.serviceBundleID, sortOrderID: a.sortOrderID, quoteItemType: a.quoteItemType }); return { result: id, message: `Successfully created quote item with ID: ${id}` }; }], ['autotask_update_quote_item', async (a) => { await s.updateQuoteItem(a.quoteItemId, { quantity: a.quantity, unitPrice: a.unitPrice, unitDiscount: a.unitDiscount, lineDiscount: a.lineDiscount, percentageDiscount: a.percentageDiscount, isOptional: a.isOptional, sortOrderID: a.sortOrderID }); return { result: true, message: `Quote item ${a.quoteItemId} updated successfully` }; }], ['autotask_delete_quote_item', async (a) => { await s.deleteQuoteItem(a.quoteId, a.quoteItemId); return { result: true, message: `Quote item ${a.quoteItemId} deleted successfully` }; }], // Picklist tools ['autotask_list_queues', async () => { const queues = await this.picklistCache.getQueues(); return { result: queues.map(q => ({ id: q.value, name: q.label, isActive: q.isActive })), message: `Found ${queues.length} queues` }; }], ['autotask_list_ticket_statuses', async () => { const statuses = await this.picklistCache.getTicketStatuses(); return { result: statuses.map(s => ({ id: s.value, name: s.label, isActive: s.isActive })), message: `Found ${statuses.length} ticket statuses` }; }], ['autotask_list_ticket_priorities', async () => { const priorities = await this.picklistCache.getTicketPriorities(); return { result: priorities.map(p => ({ id: p.value, name: p.label, isActive: p.isActive })), message: `Found ${priorities.length} ticket priorities` }; }], ['autotask_get_field_info', async (a) => { // Normalize common entity type aliases to correct Autotask REST API names const entityAliases: Record<string, string> = { 'tasks': 'ProjectTasks', 'task': 'ProjectTasks', 'projecttask': 'ProjectTasks', 'ticketnotes': 'TicketNotes', 'projectnotes': 'ProjectNotes', 'companynotes': 'CompanyNotes', }; const entityType = entityAliases[a.entityType.toLowerCase()] || a.entityType; const fields = await this.picklistCache.getFields(entityType); if (a.fieldName) { const field = fields.find(f => f.name.toLowerCase() === a.fieldName.toLowerCase()); return { result: field || null, message: field ? `Field info for ${a.entityType}.${a.fieldName}` : `Field '${a.fieldName}' not found on ${a.entityType}` }; } const summary = fields.map(f => ({ name: f.name, dataType: f.dataType, isRequired: f.isRequired, isPickList: f.isPickList, isQueryable: f.isQueryable, picklistValueCount: f.picklistValues?.length || 0 })); return { result: summary, message: `Found ${fields.length} fields for ${a.entityType}` }; }], // Billing Items (Approve and Post workflow) ['autotask_search_billing_items', async (a) => { const r = await s.searchBillingItems({ companyId: a.companyId, ticketId: a.ticketId, projectId: a.projectId, contractId: a.contractId, invoiceId: a.invoiceId, isInvoiced: a.isInvoiced, dateFrom: a.dateFrom, dateTo: a.dateTo, postedAfter: a.postedAfter, postedBefore: a.postedBefore, page: a.page, pageSize: a.pageSize } as any); return { result: r, message: `Found ${r.length} billing items` }; }], ['autotask_get_billing_item', async (a) => { const r = await s.getBillingItem(a.billingItemId); return { result: r, message: 'Billing item retrieved successfully' }; }], // Billing Item Approval Levels ['autotask_search_billing_item_approval_levels', async (a) => { const r = await s.searchBillingItemApprovalLevels({ timeEntryId: a.timeEntryId, approvalResourceId: a.approvalResourceId, approvalLevel: a.approvalLevel, approvedAfter: a.approvedAfter, approvedBefore: a.approvedBefore, page: a.page, pageSize: a.pageSize } as any); return { result: r, message: `Found ${r.length} billing item approval levels` }; }], // Time Entries ['autotask_search_time_entries', async (a) => { const r = await s.searchTimeEntries({ resourceId: a.resourceId, ticketId: a.ticketId, projectId: a.projectId, taskId: a.taskId, approvalStatus: a.approvalStatus, billable: a.billable, dateWorkedAfter: a.dateWorkedAfter, dateWorkedBefore: a.dateWorkedBefore, page: a.page, pageSize: a.pageSize } as any); return { result: r, message: `Found ${r.length} time entries` }; }], // Meta-tools for progressive discovery ['autotask_list_categories', async () => { const categories = Object.entries(TOOL_CATEGORIES).map(([name, cat]) => ({ name, description: cat.description, toolCount: cat.tools.length, })); return { result: categories, message: `Found ${categories.length} tool categories with ${Object.values(TOOL_CATEGORIES).reduce((sum, c) => sum + c.tools.length, 0)} total tools` }; }], ['autotask_list_category_tools', async (a) => { const category = TOOL_CATEGORIES[a.category]; if (!category) { const available = Object.keys(TOOL_CATEGORIES).join(', '); throw new Error(`Unknown category "${a.category}". Available: ${available}`); } const tools = TOOL_DEFINITIONS.filter(t => category.tools.includes(t.name)); return { result: tools, message: `Found ${tools.length} tools in "${a.category}" category` }; }], ['autotask_execute_tool', async (a) => { const toolName = a.toolName; const toolArgs = a.arguments || {}; const handler = this.getDispatchTable().get(toolName); if (!handler) throw new Error(`Unknown tool: ${toolName}`); // Prevent recursive meta-tool calls if (toolName === 'autotask_execute_tool') throw new Error('Cannot recursively execute autotask_execute_tool'); return handler(toolArgs); }], // Intent-based router ['autotask_router', async (a) => { const rawIntent = a.intent || ''; const suggestion = this.routeIntent(rawIntent); return { result: suggestion, message: `Suggested tool: ${suggestion.suggestedTool}` }; }], ]); } - src/handlers/tool.definitions.ts:3054-3057 (registration)Tool registered in TOOL_CATEGORIES under 'time_and_billing' category.
time_and_billing: { description: 'Time entries, billing items, and expense management', tools: ['autotask_create_time_entry', 'autotask_search_time_entries', 'autotask_search_billing_items', 'autotask_get_billing_item', 'autotask_search_billing_item_approval_levels', 'autotask_get_expense_report', 'autotask_search_expense_reports', 'autotask_create_expense_report', 'autotask_create_expense_item'] }, - Backend service method createTimeEntry that actually posts the time entry to Autotask, routing to ticket/task/project child endpoints or top-level TimeEntries for Regular Time.
async createTimeEntry(timeEntry: Partial<AutotaskTimeEntry>): Promise<number> { const http = await this.ensureClient(); try { this.logger.debug('Creating time entry:', timeEntry); // Ticket-scoped if (timeEntry.ticketID) { const id = await http.childCreate('Tickets', timeEntry.ticketID, 'TimeEntries', timeEntry); this.logger.info(`Time entry created with ID: ${id}`); return id; } // Task-scoped if (timeEntry.taskID) { const id = await http.childCreate('Tasks', timeEntry.taskID, 'TimeEntries', timeEntry); this.logger.info(`Time entry created with ID: ${id}`); return id; } // Project-scoped if (timeEntry.projectID) { const id = await http.childCreate('Projects', timeEntry.projectID, 'TimeEntries', timeEntry); this.logger.info(`Time entry created with ID: ${id}`); return id; } // Regular (no parent — meetings, admin, etc.) // Autotask accepts a POST /TimeEntries with no parent for regular entries. const id = await http.create('TimeEntries', timeEntry); this.logger.info(`Regular time entry created with ID: ${id}`); return id; } catch (error) { this.logger.error('Failed to create time entry:', error); throw error; } }