Skip to main content
Glama

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

TableJSON Schema
NameRequiredDescriptionDefault
ticketIDNoTicket ID for the time entry (omit for Regular Time)
taskIDNoTask ID for the time entry (for project work, omit for Regular Time)
projectIDNoProject ID for the time entry (omit for Regular Time)
resourceIDNoResource ID (user) logging the time. Can be omitted if resourceName is provided.
resourceNameNoName of the resource/user (e.g., "Will Spence"). Will be resolved to a resourceID automatically. Use this instead of resourceID for convenience.
categoryNoCategory 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).
dateWorkedYesDate worked (YYYY-MM-DD format)
startDateTimeNoStart date/time (ISO format)
endDateTimeNoEnd date/time (ISO format)
hoursWorkedYesNumber of hours worked
summaryNotesYesSummary notes for the time entry
internalNotesNoInternal notes for the time entry

Implementation Reference

  • 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']
      }
  • 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}` };
        }],
      ]);
    }
  • 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;
      }
    }
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Without annotations, the description effectively discloses the two operational modes and parameter dependencies (e.g., category required for regular time). It does not mention side effects or authorization, but the behavioral context is sufficient for an agent.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences with clear front-loading: first sentence defines the action, second elaborates on usage scenarios. No extraneous information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 12-parameter tool with complete schema coverage and no output schema, the description covers main usage patterns. Missing details about return value, but overall complete enough for typical use.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The description adds value beyond the 100% schema coverage by explaining the two modes and clarifying how resourceName vs resourceID work, and when category is required.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states 'Create a time entry in Autotask' and distinguishes between time entries tied to tickets/tasks/projects and 'Regular Time', making the tool's purpose unambiguous relative to sibling tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides clear guidance on when to use regular time vs. parent-linked entries and specifies required category for regular time. However, does not explicitly state when not to use this tool or compare to alternative tools.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/wyre-technology/autotask-mcp'

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