diff --git a/apps/docs/content/docs/en/tools/ashby.mdx b/apps/docs/content/docs/en/tools/ashby.mdx index f75589fa9c4..d4b71708529 100644 --- a/apps/docs/content/docs/en/tools/ashby.mdx +++ b/apps/docs/content/docs/en/tools/ashby.mdx @@ -53,7 +53,7 @@ Adds a tag to a candidate in Ashby and returns the updated candidate. | Parameter | Type | Description | | --------- | ---- | ----------- | | `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) | -| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) | +| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion\) | | `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) | | `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) | | `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) | @@ -64,20 +64,20 @@ Adds a tag to a candidate in Ashby and returns the updated candidate. | `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) | | `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) | | `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) | -| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) | +| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt\) | | `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) | | `tags` | json | List of candidate tags \(id, title, isArchived\) | | `id` | string | Resource UUID | | `name` | string | Resource name | | `title` | string | Job title or job posting title | | `status` | string | Status | -| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) | -| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) | +| `candidate` | json | Candidate summary \(id, name, primaryEmailAddress, primaryPhoneNumber\). For full candidate fields use the candidates list output or the get/create/update candidate operations. | +| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], createdAt, updatedAt\) | | `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) | | `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) | -| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) | +| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt, job \[included when expandJob=true\]\) | | `content` | string | Note content | -| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) | +| `author` | json | Note author \(id, firstName, lastName, email\) | | `isPrivate` | boolean | Whether the note is private | | `createdAt` | string | ISO 8601 creation timestamp | | `moreDataAvailable` | boolean | Whether more pages exist | @@ -102,7 +102,7 @@ Moves an application to a different interview stage. Requires an archive reason | Parameter | Type | Description | | --------- | ---- | ----------- | | `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) | -| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) | +| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion\) | | `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) | | `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) | | `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) | @@ -113,20 +113,20 @@ Moves an application to a different interview stage. Requires an archive reason | `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) | | `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) | | `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) | -| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) | +| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt\) | | `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) | | `tags` | json | List of candidate tags \(id, title, isArchived\) | | `id` | string | Resource UUID | | `name` | string | Resource name | | `title` | string | Job title or job posting title | | `status` | string | Status | -| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) | -| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) | +| `candidate` | json | Candidate summary \(id, name, primaryEmailAddress, primaryPhoneNumber\). For full candidate fields use the candidates list output or the get/create/update candidate operations. | +| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], createdAt, updatedAt\) | | `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) | | `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) | -| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) | +| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt, job \[included when expandJob=true\]\) | | `content` | string | Note content | -| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) | +| `author` | json | Note author \(id, firstName, lastName, email\) | | `isPrivate` | boolean | Whether the note is private | | `createdAt` | string | ISO 8601 creation timestamp | | `moreDataAvailable` | boolean | Whether more pages exist | @@ -155,7 +155,7 @@ Creates a new application for a candidate on a job. Optionally specify interview | Parameter | Type | Description | | --------- | ---- | ----------- | | `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) | -| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) | +| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion\) | | `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) | | `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) | | `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) | @@ -166,20 +166,20 @@ Creates a new application for a candidate on a job. Optionally specify interview | `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) | | `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) | | `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) | -| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) | +| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt\) | | `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) | | `tags` | json | List of candidate tags \(id, title, isArchived\) | | `id` | string | Resource UUID | | `name` | string | Resource name | | `title` | string | Job title or job posting title | | `status` | string | Status | -| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) | -| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) | +| `candidate` | json | Candidate summary \(id, name, primaryEmailAddress, primaryPhoneNumber\). For full candidate fields use the candidates list output or the get/create/update candidate operations. | +| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], createdAt, updatedAt\) | | `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) | | `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) | -| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) | +| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt, job \[included when expandJob=true\]\) | | `content` | string | Note content | -| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) | +| `author` | json | Note author \(id, firstName, lastName, email\) | | `isPrivate` | boolean | Whether the note is private | | `createdAt` | string | ISO 8601 creation timestamp | | `moreDataAvailable` | boolean | Whether more pages exist | @@ -200,14 +200,18 @@ Creates a new candidate record in Ashby. | `phoneNumber` | string | No | Primary phone number for the candidate | | `linkedInUrl` | string | No | LinkedIn profile URL | | `githubUrl` | string | No | GitHub profile URL | +| `website` | string | No | Personal website URL | | `sourceId` | string | No | UUID of the source to attribute the candidate to | +| `creditedToUserId` | string | No | UUID of the Ashby user to credit with sourcing this candidate | +| `createdAt` | string | No | Backdated creation timestamp in ISO 8601 \(e.g. 2024-01-01T00:00:00Z\). Defaults to now. | +| `alternateEmailAddresses` | json | No | Array of additional email address strings to add to the candidate, e.g. \["a@x.com","b@y.com"\] | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | | `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) | -| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) | +| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion\) | | `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) | | `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) | | `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) | @@ -218,20 +222,20 @@ Creates a new candidate record in Ashby. | `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) | | `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) | | `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) | -| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) | +| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt\) | | `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) | | `tags` | json | List of candidate tags \(id, title, isArchived\) | | `id` | string | Resource UUID | | `name` | string | Resource name | | `title` | string | Job title or job posting title | | `status` | string | Status | -| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) | -| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) | +| `candidate` | json | Candidate summary \(id, name, primaryEmailAddress, primaryPhoneNumber\). For full candidate fields use the candidates list output or the get/create/update candidate operations. | +| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], createdAt, updatedAt\) | | `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) | | `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) | -| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) | +| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt, job \[included when expandJob=true\]\) | | `content` | string | Note content | -| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) | +| `author` | json | Note author \(id, firstName, lastName, email\) | | `isPrivate` | boolean | Whether the note is private | | `createdAt` | string | ISO 8601 creation timestamp | | `moreDataAvailable` | boolean | Whether more pages exist | @@ -251,6 +255,8 @@ Creates a note on a candidate in Ashby. Supports plain text and HTML content (bo | `note` | string | Yes | The note content. If noteType is text/html, supports: <b>, <i>, <u>, <a>, <ul>, <ol>, <li>, <code>, <pre> | | `noteType` | string | No | Content type of the note: text/plain \(default\) or text/html | | `sendNotifications` | boolean | No | Whether to send notifications to subscribed users \(default false\) | +| `isPrivate` | boolean | No | Whether the note is private \(only visible to the author\) | +| `createdAt` | string | No | Backdated creation timestamp in ISO 8601 \(e.g. 2024-01-01T00:00:00Z\). Defaults to now. | #### Output @@ -282,7 +288,7 @@ Retrieves full details about a single application by its ID. | Parameter | Type | Description | | --------- | ---- | ----------- | | `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) | -| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) | +| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion\) | | `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) | | `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) | | `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) | @@ -293,20 +299,20 @@ Retrieves full details about a single application by its ID. | `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) | | `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) | | `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) | -| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) | +| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt\) | | `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) | | `tags` | json | List of candidate tags \(id, title, isArchived\) | | `id` | string | Resource UUID | | `name` | string | Resource name | | `title` | string | Job title or job posting title | | `status` | string | Status | -| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) | -| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) | +| `candidate` | json | Candidate summary \(id, name, primaryEmailAddress, primaryPhoneNumber\). For full candidate fields use the candidates list output or the get/create/update candidate operations. | +| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], createdAt, updatedAt\) | | `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) | | `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) | -| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) | +| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt, job \[included when expandJob=true\]\) | | `content` | string | Note content | -| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) | +| `author` | json | Note author \(id, firstName, lastName, email\) | | `isPrivate` | boolean | Whether the note is private | | `createdAt` | string | ISO 8601 creation timestamp | | `moreDataAvailable` | boolean | Whether more pages exist | @@ -329,7 +335,7 @@ Retrieves full details about a single candidate by their ID. | Parameter | Type | Description | | --------- | ---- | ----------- | | `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) | -| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) | +| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion\) | | `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) | | `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) | | `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) | @@ -340,20 +346,20 @@ Retrieves full details about a single candidate by their ID. | `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) | | `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) | | `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) | -| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) | +| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt\) | | `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) | | `tags` | json | List of candidate tags \(id, title, isArchived\) | | `id` | string | Resource UUID | | `name` | string | Resource name | | `title` | string | Job title or job posting title | | `status` | string | Status | -| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) | -| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) | +| `candidate` | json | Candidate summary \(id, name, primaryEmailAddress, primaryPhoneNumber\). For full candidate fields use the candidates list output or the get/create/update candidate operations. | +| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], createdAt, updatedAt\) | | `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) | | `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) | -| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) | +| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt, job \[included when expandJob=true\]\) | | `content` | string | Note content | -| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) | +| `author` | json | Note author \(id, firstName, lastName, email\) | | `isPrivate` | boolean | Whether the note is private | | `createdAt` | string | ISO 8601 creation timestamp | | `moreDataAvailable` | boolean | Whether more pages exist | @@ -376,7 +382,7 @@ Retrieves full details about a single job by its ID. | Parameter | Type | Description | | --------- | ---- | ----------- | | `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) | -| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) | +| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion\) | | `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) | | `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) | | `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) | @@ -387,20 +393,20 @@ Retrieves full details about a single job by its ID. | `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) | | `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) | | `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) | -| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) | +| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt\) | | `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) | | `tags` | json | List of candidate tags \(id, title, isArchived\) | | `id` | string | Resource UUID | | `name` | string | Resource name | | `title` | string | Job title or job posting title | | `status` | string | Status | -| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) | -| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) | +| `candidate` | json | Candidate summary \(id, name, primaryEmailAddress, primaryPhoneNumber\). For full candidate fields use the candidates list output or the get/create/update candidate operations. | +| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], createdAt, updatedAt\) | | `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) | | `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) | -| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) | +| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt, job \[included when expandJob=true\]\) | | `content` | string | Note content | -| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) | +| `author` | json | Note author \(id, firstName, lastName, email\) | | `isPrivate` | boolean | Whether the note is private | | `createdAt` | string | ISO 8601 creation timestamp | | `moreDataAvailable` | boolean | Whether more pages exist | @@ -417,8 +423,8 @@ Retrieves full details about a single job posting by its ID. | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Ashby API Key | | `jobPostingId` | string | Yes | The UUID of the job posting to fetch | -| `expandApplicationFormDefinition` | boolean | No | Include application form definition in the response | -| `expandSurveyFormDefinitions` | boolean | No | Include survey form definitions in the response | +| `jobBoardId` | string | No | Optional job board UUID. If omitted, returns posting for the external job board. | +| `expandJob` | boolean | No | Whether to expand and include the related job object in the response | #### Output @@ -474,8 +480,9 @@ Retrieves full details about a single job posting by its ID. | ↳ `minValue` | number | Minimum value | | ↳ `maxValue` | number | Maximum value | | ↳ `shouldDisplayCompensationOnJobBoard` | boolean | Whether compensation is shown on the job board | -| `applicationLimitCalloutHtml` | string | HTML callout shown when application limit is reached | +| `applicationLimitCalloutHtml` | string | HTML callout shown when the application limit is reached | | `updatedAt` | string | ISO 8601 last update timestamp | +| `job` | object | The expanded job object, only present when the request was made with expandJob=true | ### `ashby_get_offer` @@ -493,7 +500,7 @@ Retrieves full details about a single offer by its ID. | Parameter | Type | Description | | --------- | ---- | ----------- | | `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) | -| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) | +| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion\) | | `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) | | `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) | | `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) | @@ -504,20 +511,20 @@ Retrieves full details about a single offer by its ID. | `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) | | `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) | | `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) | -| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) | +| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt\) | | `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) | | `tags` | json | List of candidate tags \(id, title, isArchived\) | | `id` | string | Resource UUID | | `name` | string | Resource name | | `title` | string | Job title or job posting title | | `status` | string | Status | -| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) | -| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) | +| `candidate` | json | Candidate summary \(id, name, primaryEmailAddress, primaryPhoneNumber\). For full candidate fields use the candidates list output or the get/create/update candidate operations. | +| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], createdAt, updatedAt\) | | `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) | | `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) | -| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) | +| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt, job \[included when expandJob=true\]\) | | `content` | string | Note content | -| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) | +| `author` | json | Note author \(id, firstName, lastName, email\) | | `isPrivate` | boolean | Whether the note is private | | `createdAt` | string | ISO 8601 creation timestamp | | `moreDataAvailable` | boolean | Whether more pages exist | @@ -537,6 +544,7 @@ Lists all applications in an Ashby organization with pagination and optional fil | `perPage` | number | No | Number of results per page \(default 100\) | | `status` | string | No | Filter by application status: Active, Hired, Archived, or Lead | | `jobId` | string | No | Filter applications by a specific job UUID | +| `candidateId` | string | No | Filter applications by a specific candidate UUID | | `createdAfter` | string | No | Filter to applications created after this ISO 8601 timestamp \(e.g. 2024-01-01T00:00:00Z\) | #### Output @@ -605,6 +613,7 @@ Lists all candidates in an Ashby organization with cursor-based pagination. | `apiKey` | string | Yes | Ashby API Key | | `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value | | `perPage` | number | No | Number of results per page \(default 100\) | +| `createdAfter` | string | No | Only return candidates created after this ISO 8601 timestamp \(e.g. 2024-01-01T00:00:00Z\) | #### Output @@ -623,6 +632,10 @@ Lists all custom field definitions configured in Ashby. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Ashby API Key | +| `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value | +| `perPage` | number | No | Number of results per page \(default and max 100\) | +| `syncToken` | string | No | Opaque token from a prior sync to fetch only items changed since then | +| `includeArchived` | boolean | No | When true, includes archived custom fields in results \(default false\) | #### Output @@ -640,6 +653,9 @@ Lists all custom field definitions configured in Ashby. | ↳ `label` | string | Display label | | ↳ `value` | string | Stored value | | ↳ `isArchived` | boolean | Whether archived | +| `moreDataAvailable` | boolean | Whether more pages of results exist | +| `nextCursor` | string | Opaque cursor for fetching the next page | +| `syncToken` | string | Opaque sync token returned after the last page; pass on next sync | ### `ashby_list_departments` @@ -650,6 +666,10 @@ Lists all departments in Ashby. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Ashby API Key | +| `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value | +| `perPage` | number | No | Number of results per page \(default and max 100\) | +| `syncToken` | string | No | Opaque token from a prior sync to fetch only items changed since then | +| `includeArchived` | boolean | No | When true, includes archived departments in results \(default false\) | #### Output @@ -663,6 +683,10 @@ Lists all departments in Ashby. | ↳ `parentId` | string | Parent department UUID | | ↳ `createdAt` | string | ISO 8601 creation timestamp | | ↳ `updatedAt` | string | ISO 8601 last update timestamp | +| ↳ `extraData` | json | Free-form key-value metadata | +| `moreDataAvailable` | boolean | Whether more pages of results exist | +| `nextCursor` | string | Opaque cursor for fetching the next page | +| `syncToken` | string | Opaque sync token returned after the last page; pass on next sync | ### `ashby_list_interviews` @@ -677,6 +701,7 @@ Lists interview schedules in Ashby, optionally filtered by application or interv | `interviewStageId` | string | No | The UUID of the interview stage to list interview schedules for | | `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value | | `perPage` | number | No | Number of results per page \(default 100\) | +| `createdAfter` | string | No | Only return interview schedules created after this ISO 8601 timestamp \(e.g. 2024-01-01T00:00:00Z\) | #### Output @@ -714,6 +739,10 @@ Lists all job postings in Ashby. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Ashby API Key | +| `location` | string | No | Filter by location name \(case sensitive\) | +| `department` | string | No | Filter by department name \(case sensitive\) | +| `listedOnly` | boolean | No | When true, only returns listed \(publicly visible\) job postings \(default false\) | +| `jobBoardId` | string | No | UUID of a specific job board to filter postings to. If omitted, returns postings on the primary external job board. | #### Output @@ -752,6 +781,11 @@ Lists all jobs in an Ashby organization. By default returns Open, Closed, and Ar | `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value | | `perPage` | number | No | Number of results per page \(default 100\) | | `status` | string | No | Filter by job status: Open, Closed, Archived, or Draft | +| `createdAfter` | string | No | Only return jobs created after this ISO 8601 timestamp \(e.g. 2024-01-01T00:00:00Z\) | +| `openedAfter` | string | No | Only return jobs opened after this ISO 8601 timestamp | +| `openedBefore` | string | No | Only return jobs opened before this ISO 8601 timestamp | +| `closedAfter` | string | No | Only return jobs closed after this ISO 8601 timestamp | +| `closedBefore` | string | No | Only return jobs closed before this ISO 8601 timestamp | #### Output @@ -770,6 +804,11 @@ Lists all locations configured in Ashby. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Ashby API Key | +| `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value | +| `perPage` | number | No | Number of results per page \(default and max 100\) | +| `syncToken` | string | No | Opaque token from a prior sync to fetch only items changed since then | +| `includeArchived` | boolean | No | When true, includes archived locations in results \(default false\) | +| `includeLocationHierarchy` | boolean | No | When true, includes location hierarchy components/regions \(default false\) | #### Output @@ -790,6 +829,10 @@ Lists all locations configured in Ashby. | ↳ `addressLocality` | string | City or locality | | ↳ `postalCode` | string | Postal code | | ↳ `streetAddress` | string | Street address | +| ↳ `extraData` | json | Free-form key-value metadata | +| `moreDataAvailable` | boolean | Whether more pages of results exist | +| `nextCursor` | string | Opaque cursor for fetching the next page | +| `syncToken` | string | Opaque sync token returned after the last page; pass on next sync | ### `ashby_list_notes` @@ -832,6 +875,9 @@ Lists all offers with their latest version in an Ashby organization. | `apiKey` | string | Yes | Ashby API Key | | `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value | | `perPage` | number | No | Number of results per page | +| `createdAfter` | string | No | Only return offers created after this ISO 8601 timestamp \(e.g. 2024-01-01T00:00:00Z\) | +| `syncToken` | string | No | Opaque token from a prior sync to fetch only items changed since then | +| `applicationId` | string | No | Return only offers for the specified application UUID | #### Output @@ -852,6 +898,7 @@ Lists all openings in Ashby with pagination. | `apiKey` | string | Yes | Ashby API Key | | `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value | | `perPage` | number | No | Number of results per page \(default 100\) | +| `createdAfter` | string | No | Only return openings created after this ISO 8601 timestamp \(e.g. 2024-01-01T00:00:00Z\) | #### Output @@ -869,6 +916,7 @@ Lists all candidate sources configured in Ashby. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Ashby API Key | +| `includeArchived` | boolean | No | When true, includes archived sources in results \(default false\) | #### Output @@ -894,6 +942,7 @@ Lists all users in Ashby with pagination. | `apiKey` | string | Yes | Ashby API Key | | `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value | | `perPage` | number | No | Number of results per page \(default 100\) | +| `includeDeactivated` | boolean | No | When true, includes deactivated users in results \(default false\) | #### Output @@ -920,7 +969,7 @@ Removes a tag from a candidate in Ashby and returns the updated candidate. | Parameter | Type | Description | | --------- | ---- | ----------- | | `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) | -| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) | +| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion\) | | `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) | | `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) | | `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) | @@ -931,20 +980,20 @@ Removes a tag from a candidate in Ashby and returns the updated candidate. | `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) | | `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) | | `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) | -| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) | +| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt\) | | `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) | | `tags` | json | List of candidate tags \(id, title, isArchived\) | | `id` | string | Resource UUID | | `name` | string | Resource name | | `title` | string | Job title or job posting title | | `status` | string | Status | -| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) | -| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) | +| `candidate` | json | Candidate summary \(id, name, primaryEmailAddress, primaryPhoneNumber\). For full candidate fields use the candidates list output or the get/create/update candidate operations. | +| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], createdAt, updatedAt\) | | `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) | | `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) | -| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) | +| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt, job \[included when expandJob=true\]\) | | `content` | string | Note content | -| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) | +| `author` | json | Note author \(id, firstName, lastName, email\) | | `isPrivate` | boolean | Whether the note is private | | `createdAt` | string | ISO 8601 creation timestamp | | `moreDataAvailable` | boolean | Whether more pages exist | @@ -985,14 +1034,19 @@ Updates an existing candidate record in Ashby. Only provided fields are changed. | `linkedInUrl` | string | No | LinkedIn profile URL | | `githubUrl` | string | No | GitHub profile URL | | `websiteUrl` | string | No | Personal website URL | +| `alternateEmail` | string | No | An additional email address to add to the candidate | | `sourceId` | string | No | UUID of the source to attribute the candidate to | +| `creditedToUserId` | string | No | UUID of the Ashby user to credit with sourcing this candidate | +| `createdAt` | string | No | Backdated creation timestamp in ISO 8601. Only updatable if originally backdated. | +| `sendNotifications` | boolean | No | Whether to send a notification when the source is updated \(default true\) | +| `socialLinks` | json | No | Array of social link objects to set on the candidate, e.g. \[\{"type":"LinkedIn","url":"https://..."\}\]. Replaces existing social links. | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | | `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) | -| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) | +| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion\) | | `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) | | `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) | | `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) | @@ -1003,20 +1057,20 @@ Updates an existing candidate record in Ashby. Only provided fields are changed. | `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) | | `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) | | `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) | -| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) | +| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt\) | | `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) | | `tags` | json | List of candidate tags \(id, title, isArchived\) | | `id` | string | Resource UUID | | `name` | string | Resource name | | `title` | string | Job title or job posting title | | `status` | string | Status | -| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) | -| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) | +| `candidate` | json | Candidate summary \(id, name, primaryEmailAddress, primaryPhoneNumber\). For full candidate fields use the candidates list output or the get/create/update candidate operations. | +| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], createdAt, updatedAt\) | | `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) | | `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) | -| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) | +| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt, job \[included when expandJob=true\]\) | | `content` | string | Note content | -| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) | +| `author` | json | Note author \(id, firstName, lastName, email\) | | `isPrivate` | boolean | Whether the note is private | | `createdAt` | string | ISO 8601 creation timestamp | | `moreDataAvailable` | boolean | Whether more pages exist | diff --git a/apps/docs/content/docs/en/tools/confluence.mdx b/apps/docs/content/docs/en/tools/confluence.mdx index 8ffb145f862..967275020e4 100644 --- a/apps/docs/content/docs/en/tools/confluence.mdx +++ b/apps/docs/content/docs/en/tools/confluence.mdx @@ -81,7 +81,6 @@ Update a Confluence page using the Confluence API. | `pageId` | string | Yes | Confluence page ID to update \(numeric ID from page URL or API\) | | `title` | string | No | New title for the page | | `content` | string | No | New content for the page in Confluence storage format | -| `version` | number | No | Version number of the page \(required for preventing conflicts\) | | `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -1091,6 +1090,8 @@ Delete a Confluence space. | `ts` | string | ISO 8601 timestamp of the operation | | `spaceId` | string | Deleted space ID | | `deleted` | boolean | Deletion status | +| `longTaskId` | string | ID of the long-running deletion task; poll Confluence long-task API to track completion | +| `longTaskStatusLink` | string | Relative link to the long-task status endpoint | ### `confluence_list_spaces` diff --git a/apps/docs/content/docs/en/tools/firecrawl.mdx b/apps/docs/content/docs/en/tools/firecrawl.mdx index 6c582bfc76d..84ccd439563 100644 --- a/apps/docs/content/docs/en/tools/firecrawl.mdx +++ b/apps/docs/content/docs/en/tools/firecrawl.mdx @@ -254,6 +254,8 @@ Parse uploaded documents (PDF, DOCX, HTML, etc.) into clean markdown using Firec | `proxy` | string | No | Proxy mode: "basic" or "auto" | | `zeroDataRetention` | boolean | No | Enable zero data retention. Defaults to false. | | `apiKey` | string | Yes | Firecrawl API key | +| `pricing` | custom | No | No description | +| `metadata` | string | No | No description | | `rateLimit` | string | No | No description | #### Output diff --git a/apps/docs/content/docs/en/tools/google_drive.mdx b/apps/docs/content/docs/en/tools/google_drive.mdx index 08298a9d066..629296fe92d 100644 --- a/apps/docs/content/docs/en/tools/google_drive.mdx +++ b/apps/docs/content/docs/en/tools/google_drive.mdx @@ -146,6 +146,78 @@ Get metadata for a specific file in Google Drive by its ID | ↳ `md5Checksum` | string | MD5 hash | | ↳ `version` | string | Version number | +### `google_drive_get_content` + +Get content from a file in Google Drive with complete metadata (exports Google Workspace files automatically) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | Yes | The ID of the file to get content from | +| `mimeType` | string | No | The MIME type to export Google Workspace files to \(optional\) | +| `includeRevisions` | boolean | No | Whether to include revision history in the metadata \(default: true, returns first 100 revisions\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | File content as text \(Google Workspace files are exported\) | +| `metadata` | object | Complete file metadata from Google Drive | +| ↳ `id` | string | Google Drive file ID | +| ↳ `kind` | string | Resource type identifier | +| ↳ `name` | string | File name | +| ↳ `mimeType` | string | MIME type | +| ↳ `description` | string | File description | +| ↳ `originalFilename` | string | Original uploaded filename | +| ↳ `fullFileExtension` | string | Full file extension | +| ↳ `fileExtension` | string | File extension | +| ↳ `owners` | json | List of file owners | +| ↳ `permissions` | json | File permissions | +| ↳ `permissionIds` | json | Permission IDs | +| ↳ `shared` | boolean | Whether file is shared | +| ↳ `ownedByMe` | boolean | Whether owned by current user | +| ↳ `writersCanShare` | boolean | Whether writers can share | +| ↳ `viewersCanCopyContent` | boolean | Whether viewers can copy | +| ↳ `copyRequiresWriterPermission` | boolean | Whether copy requires writer permission | +| ↳ `sharingUser` | json | User who shared the file | +| ↳ `starred` | boolean | Whether file is starred | +| ↳ `trashed` | boolean | Whether file is in trash | +| ↳ `explicitlyTrashed` | boolean | Whether explicitly trashed | +| ↳ `appProperties` | json | App-specific properties | +| ↳ `createdTime` | string | File creation time | +| ↳ `modifiedTime` | string | Last modification time | +| ↳ `modifiedByMeTime` | string | When modified by current user | +| ↳ `viewedByMeTime` | string | When last viewed by current user | +| ↳ `sharedWithMeTime` | string | When shared with current user | +| ↳ `lastModifyingUser` | json | User who last modified the file | +| ↳ `viewedByMe` | boolean | Whether viewed by current user | +| ↳ `modifiedByMe` | boolean | Whether modified by current user | +| ↳ `webViewLink` | string | URL to view in browser | +| ↳ `webContentLink` | string | Direct download URL | +| ↳ `iconLink` | string | URL to file icon | +| ↳ `thumbnailLink` | string | URL to thumbnail | +| ↳ `exportLinks` | json | Export format links | +| ↳ `size` | string | File size in bytes | +| ↳ `quotaBytesUsed` | string | Storage quota used | +| ↳ `md5Checksum` | string | MD5 hash | +| ↳ `sha1Checksum` | string | SHA-1 hash | +| ↳ `sha256Checksum` | string | SHA-256 hash | +| ↳ `parents` | json | Parent folder IDs | +| ↳ `spaces` | json | Spaces containing file | +| ↳ `driveId` | string | Shared drive ID | +| ↳ `capabilities` | json | User capabilities on file | +| ↳ `version` | string | Version number | +| ↳ `headRevisionId` | string | Head revision ID | +| ↳ `hasThumbnail` | boolean | Whether has thumbnail | +| ↳ `thumbnailVersion` | string | Thumbnail version | +| ↳ `imageMediaMetadata` | json | Image-specific metadata | +| ↳ `videoMediaMetadata` | json | Video-specific metadata | +| ↳ `isAppAuthorized` | boolean | Whether created by requesting app | +| ↳ `contentRestrictions` | json | Content restrictions | +| ↳ `linkShareMetadata` | json | Link share metadata | +| ↳ `revisions` | json | File revision history \(first 100 revisions only\) | + ### `google_drive_create_folder` Create a new folder in Google Drive with complete metadata returned @@ -375,6 +447,79 @@ Create a copy of a file in Google Drive | ↳ `owners` | json | List of file owners | | ↳ `size` | string | File size in bytes | +### `google_drive_move` + +Move a file or folder to a different folder in Google Drive + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | Yes | The ID of the file or folder to move | +| `destinationFolderId` | string | Yes | The ID of the destination folder | +| `removeFromCurrent` | boolean | No | Whether to remove the file from its current parent folder \(default: true\). Set to false to add the file to the destination without removing it from the current location. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `file` | json | The moved file metadata | +| ↳ `id` | string | Google Drive file ID | +| ↳ `kind` | string | Resource type identifier | +| ↳ `name` | string | File name | +| ↳ `mimeType` | string | MIME type | +| ↳ `webViewLink` | string | URL to view in browser | +| ↳ `parents` | json | Parent folder IDs | +| ↳ `createdTime` | string | File creation time | +| ↳ `modifiedTime` | string | Last modification time | +| ↳ `owners` | json | List of file owners | +| ↳ `size` | string | File size in bytes | + +### `google_drive_search` + +Search for files in Google Drive using advanced query syntax (e.g., fullText contains, mimeType, modifiedTime, etc.) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `query` | string | Yes | Google Drive query string using advanced search syntax \(e.g., "fullText contains \'budget\'", "mimeType = \'application/pdf\'", "modifiedTime > \'2024-01-01\'"\) | +| `pageSize` | number | No | Maximum number of files to return \(default: 100\) | +| `pageToken` | string | No | Token for fetching the next page of results | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `files` | array | Array of file metadata objects matching the search query | +| ↳ `id` | string | Google Drive file ID | +| ↳ `kind` | string | Resource type identifier | +| ↳ `name` | string | File name | +| ↳ `mimeType` | string | MIME type | +| ↳ `description` | string | File description | +| ↳ `originalFilename` | string | Original uploaded filename | +| ↳ `fullFileExtension` | string | Full file extension | +| ↳ `fileExtension` | string | File extension | +| ↳ `owners` | json | List of file owners | +| ↳ `permissions` | json | File permissions | +| ↳ `shared` | boolean | Whether file is shared | +| ↳ `ownedByMe` | boolean | Whether owned by current user | +| ↳ `starred` | boolean | Whether file is starred | +| ↳ `trashed` | boolean | Whether file is in trash | +| ↳ `createdTime` | string | File creation time | +| ↳ `modifiedTime` | string | Last modification time | +| ↳ `lastModifyingUser` | json | User who last modified the file | +| ↳ `webViewLink` | string | URL to view in browser | +| ↳ `webContentLink` | string | Direct download URL | +| ↳ `iconLink` | string | URL to file icon | +| ↳ `thumbnailLink` | string | URL to thumbnail | +| ↳ `size` | string | File size in bytes | +| ↳ `parents` | json | Parent folder IDs | +| ↳ `driveId` | string | Shared drive ID | +| ↳ `capabilities` | json | User capabilities on file | +| ↳ `version` | string | Version number | +| `nextPageToken` | string | Token for fetching the next page of results | + ### `google_drive_update` Update file metadata in Google Drive (rename, move, star, add description) @@ -428,6 +573,29 @@ Move a file to the trash in Google Drive (can be restored later) | ↳ `trashedTime` | string | When file was trashed | | ↳ `webViewLink` | string | URL to view in browser | +### `google_drive_untrash` + +Restore a file from the trash in Google Drive + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | Yes | The ID of the file to restore from trash | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `file` | json | The restored file metadata | +| ↳ `id` | string | Google Drive file ID | +| ↳ `kind` | string | Resource type identifier | +| ↳ `name` | string | File name | +| ↳ `mimeType` | string | MIME type | +| ↳ `trashed` | boolean | Whether file is in trash \(should be false\) | +| ↳ `webViewLink` | string | URL to view in browser | +| ↳ `parents` | json | Parent folder IDs | + ### `google_drive_delete` Permanently delete a file from Google Drive (bypasses trash) @@ -505,6 +673,7 @@ List all permissions (who has access) for a file in Google Drive | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `fileId` | string | Yes | The ID of the file to list permissions for | +| `pageToken` | string | No | The page token to use for pagination | #### Output diff --git a/apps/docs/content/docs/en/tools/jira.mdx b/apps/docs/content/docs/en/tools/jira.mdx index 17742deeda2..7164372612f 100644 --- a/apps/docs/content/docs/en/tools/jira.mdx +++ b/apps/docs/content/docs/en/tools/jira.mdx @@ -384,7 +384,7 @@ Assign a Jira issue to a user | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | | `issueKey` | string | Yes | Jira issue key to assign \(e.g., PROJ-123\) | -| `accountId` | string | Yes | Account ID of the user to assign the issue to. Use "-1" for automatic assignment or null to unassign. | +| `accountId` | string | Yes | Account ID of the user to assign the issue to. Use "-1" for automatic assignment, or leave empty / pass "null" to unassign. | | `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -436,7 +436,7 @@ Search for Jira issues using JQL (Jira Query Language) | `jql` | string | Yes | JQL query string to search for issues \(e.g., "project = PROJ AND status = Open"\) | | `nextPageToken` | string | No | Cursor token for the next page of results. Omit for the first page. | | `maxResults` | number | No | Maximum number of results to return per page \(default: 50\) | -| `fields` | array | No | Array of field names to return \(default: all navigable\). Use "*all" for every field. | +| `fields` | array | No | Array of field names to return \(default: all fields\). | | `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -506,7 +506,7 @@ Search for Jira issues using JQL (Jira Query Language) | ↳ `updated` | string | ISO 8601 timestamp when the issue was last updated | | `nextPageToken` | string | Cursor token for the next page. Null when no more results. | | `isLast` | boolean | Whether this is the last page of results | -| `total` | number | Total number of matching issues \(may not always be available\) | +| `total` | number | Always null. The Jira /search/jql endpoint does not return a total count; use isLast and nextPageToken for pagination. | ### `jira_add_comment` diff --git a/apps/docs/content/docs/en/tools/jira_service_management.mdx b/apps/docs/content/docs/en/tools/jira_service_management.mdx index 6b73750e17c..04aed8404ca 100644 --- a/apps/docs/content/docs/en/tools/jira_service_management.mdx +++ b/apps/docs/content/docs/en/tools/jira_service_management.mdx @@ -331,8 +331,7 @@ Add customers to a service desk in Jira Service Management | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | | `cloudId` | string | No | Jira Cloud ID for the instance | | `serviceDeskId` | string | Yes | Service Desk ID \(e.g., "1", "2"\) | -| `accountIds` | string | No | Comma-separated Atlassian account IDs to add as customers | -| `emails` | string | No | Comma-separated email addresses to add as customers | +| `accountIds` | string | Yes | Comma-separated Atlassian account IDs to add as customers | #### Output diff --git a/apps/docs/content/docs/en/tools/slack.mdx b/apps/docs/content/docs/en/tools/slack.mdx index 61884d2cc6e..54065a58200 100644 --- a/apps/docs/content/docs/en/tools/slack.mdx +++ b/apps/docs/content/docs/en/tools/slack.mdx @@ -170,8 +170,6 @@ Create and share Slack canvases in channels. Canvases are collaborative document | Parameter | Type | Description | | --------- | ---- | ----------- | | `canvas_id` | string | Unique canvas identifier | -| `channel` | string | Channel where canvas was created | -| `title` | string | Canvas title | ### `slack_message_reader` @@ -522,6 +520,7 @@ List all channels in a Slack workspace. Returns public and private channels the | `includePrivate` | boolean | No | Include private channels the bot is a member of \(default: true\) | | `excludeArchived` | boolean | No | Exclude archived channels \(default: true\) | | `limit` | number | No | Maximum number of channels to return \(default: 100, max: 200\) | +| `cursor` | string | No | Pagination cursor from a previous response.next_cursor | #### Output @@ -547,6 +546,7 @@ List all channels in a Slack workspace. Returns public and private channels the | `ids` | array | Array of channel IDs for easy access | | `names` | array | Array of channel names for easy access | | `count` | number | Total number of channels returned | +| `nextCursor` | string | Cursor for the next page; null if no more pages | ### `slack_list_members` @@ -560,6 +560,7 @@ List all members (user IDs) in a Slack channel. Use with Get User Info to resolv | `botToken` | string | No | Bot token for Custom Bot | | `channel` | string | Yes | Channel ID to list members from | | `limit` | number | No | Maximum number of members to return \(default: 100, max: 200\) | +| `cursor` | string | No | Pagination cursor from a previous response.next_cursor | #### Output @@ -567,6 +568,7 @@ List all members (user IDs) in a Slack channel. Use with Get User Info to resolv | --------- | ---- | ----------- | | `members` | array | Array of user IDs who are members of the channel \(e.g., U1234567890\) | | `count` | number | Total number of members returned | +| `nextCursor` | string | Cursor for the next page; null if no more pages | ### `slack_list_users` @@ -580,6 +582,7 @@ List all users in a Slack workspace. Returns user profiles with names and avatar | `botToken` | string | No | Bot token for Custom Bot | | `includeDeleted` | boolean | No | Include deactivated/deleted users \(default: false\) | | `limit` | number | No | Maximum number of users to return \(default: 100, max: 200\) | +| `cursor` | string | No | Pagination cursor from a previous response.next_cursor | #### Output @@ -602,6 +605,7 @@ List all users in a Slack workspace. Returns user profiles with names and avatar | `ids` | array | Array of user IDs for easy access | | `names` | array | Array of usernames for easy access | | `count` | number | Total number of users returned | +| `nextCursor` | string | Cursor for the next page; null if no more pages | ### `slack_get_user` @@ -638,7 +642,6 @@ Get detailed information about a specific Slack user by their user ID. | ↳ `is_restricted` | boolean | Whether the user is a guest \(restricted\) | | ↳ `is_ultra_restricted` | boolean | Whether the user is a single-channel guest | | ↳ `is_app_user` | boolean | Whether user is an app user | -| ↳ `is_stranger` | boolean | Whether user is from different workspace | | ↳ `deleted` | boolean | Whether the user is deactivated | | ↳ `color` | string | User color for display | | ↳ `timezone` | string | Timezone identifier \(e.g., America/Los_Angeles\) | diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 4239620a845..552ab0335d5 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -5077,10 +5077,18 @@ "name": "List Files", "description": "List Google Drive files" }, + { + "name": "Search Files", + "description": "Search for files in Google Drive using advanced query syntax (e.g., fullText contains, mimeType, modifiedTime, etc.)" + }, { "name": "Get File Info", "description": "Get metadata for a specific file in Google Drive by its ID" }, + { + "name": "Get File Content", + "description": "Get content from a file in Google Drive with complete metadata (exports Google Workspace files automatically)" + }, { "name": "Create Folder", "description": "Create a new folder in Google Drive with complete metadata returned" @@ -5101,6 +5109,10 @@ "name": "Copy File", "description": "Create a copy of a file in Google Drive" }, + { + "name": "Move File", + "description": "Move a file or folder to a different folder in Google Drive" + }, { "name": "Update File", "description": "Update file metadata in Google Drive (rename, move, star, add description)" @@ -5109,6 +5121,10 @@ "name": "Move to Trash", "description": "Move a file to the trash in Google Drive (can be restored later)" }, + { + "name": "Restore from Trash", + "description": "Restore a file from the trash in Google Drive" + }, { "name": "Delete Permanently", "description": "Permanently delete a file from Google Drive (bypasses trash)" @@ -5130,7 +5146,7 @@ "description": "Get information about the user and their Google Drive (storage quota, capabilities)" } ], - "operationCount": 14, + "operationCount": 18, "triggers": [ { "id": "google_drive_poller", @@ -6962,6 +6978,10 @@ "name": "Read Issue", "description": "Retrieve detailed information about a specific Jira issue" }, + { + "name": "Read Bulk Issues", + "description": "Retrieve multiple Jira issues from a project in bulk" + }, { "name": "Update Issue", "description": "Update a Jira issue" @@ -7055,7 +7075,7 @@ "description": "Search for Jira users by email address or display name. Returns matching users with their accountId, displayName, and emailAddress." } ], - "operationCount": 24, + "operationCount": 25, "triggers": [ { "id": "jira_issue_created", diff --git a/apps/sim/app/api/tools/confluence/comment/route.ts b/apps/sim/app/api/tools/confluence/comment/route.ts index bf1adac74d8..02948bce7a3 100644 --- a/apps/sim/app/api/tools/confluence/comment/route.ts +++ b/apps/sim/app/api/tools/confluence/comment/route.ts @@ -73,15 +73,26 @@ export const PUT = withRouteHandler(async (request: NextRequest) => { return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) } - // Get current comment version - const getUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/footer-comments/${commentId}` - const getResponse = await fetch(getUrl, { + // Detect comment type — try footer-comments first, fall back to inline-comments + const apiBase = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2` + let commentEndpoint = 'footer-comments' + let getResponse = await fetch(`${apiBase}/footer-comments/${commentId}`, { headers: { Accept: 'application/json', Authorization: `Bearer ${accessToken}`, }, }) + if (getResponse.status === 404) { + commentEndpoint = 'inline-comments' + getResponse = await fetch(`${apiBase}/inline-comments/${commentId}`, { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + } + if (!getResponse.ok) { const errorText = await getResponse.text() throw new Error( @@ -92,7 +103,7 @@ export const PUT = withRouteHandler(async (request: NextRequest) => { const currentComment = await getResponse.json() const currentVersion = currentComment.version?.number || 1 - const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/footer-comments/${commentId}` + const url = `${apiBase}/${commentEndpoint}/${commentId}` const updateBody = { body: { @@ -164,9 +175,48 @@ export const DELETE = withRouteHandler(async (request: NextRequest) => { return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) } - const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/footer-comments/${commentId}` + const apiBase = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2` - const response = await fetch(url, { + // Detect comment type with a non-destructive GET so a 404 from a prior + // deletion isn't masked by a second DELETE attempt against the wrong endpoint. + let commentEndpoint = 'footer-comments' + let detectResponse = await fetch(`${apiBase}/footer-comments/${commentId}`, { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (detectResponse.status === 404) { + commentEndpoint = 'inline-comments' + detectResponse = await fetch(`${apiBase}/inline-comments/${commentId}`, { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + } + + if (!detectResponse.ok) { + const errorText = await detectResponse.text() + logger.error('Confluence API error response:', { + status: detectResponse.status, + statusText: detectResponse.statusText, + error: errorText, + }) + return NextResponse.json( + { + error: parseAtlassianErrorMessage( + detectResponse.status, + detectResponse.statusText, + errorText + ), + }, + { status: detectResponse.status } + ) + } + + const response = await fetch(`${apiBase}/${commentEndpoint}/${commentId}`, { method: 'DELETE', headers: { Accept: 'application/json', diff --git a/apps/sim/app/api/tools/confluence/page-properties/route.ts b/apps/sim/app/api/tools/confluence/page-properties/route.ts index 0fc5fe25426..56e7b3b601a 100644 --- a/apps/sim/app/api/tools/confluence/page-properties/route.ts +++ b/apps/sim/app/api/tools/confluence/page-properties/route.ts @@ -28,7 +28,7 @@ const updatePropertySchema = z.object({ propertyId: z.string().min(1, 'Property ID is required'), key: z.string().min(1, 'Property key is required'), value: z.any(), - versionNumber: z.number().min(1, 'Version number is required'), + versionNumber: z.number().min(1).optional(), }) const deletePropertySchema = z.object({ @@ -256,6 +256,39 @@ export const PUT = withRouteHandler(async (request: NextRequest) => { const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/properties/${propertyId}` + let nextVersion = versionNumber + if (nextVersion === undefined) { + const lookupResponse = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + if (!lookupResponse.ok) { + const errorText = await lookupResponse.text() + return NextResponse.json( + { + error: parseAtlassianErrorMessage( + lookupResponse.status, + lookupResponse.statusText, + errorText + ), + }, + { status: lookupResponse.status } + ) + } + const current = await lookupResponse.json() + const currentNumber = current?.version?.number + if (typeof currentNumber !== 'number') { + return NextResponse.json( + { error: 'Could not determine current property version' }, + { status: 500 } + ) + } + nextVersion = currentNumber + 1 + } + const response = await fetch(url, { method: 'PUT', headers: { @@ -266,7 +299,7 @@ export const PUT = withRouteHandler(async (request: NextRequest) => { body: JSON.stringify({ key, value, - version: { number: versionNumber }, + version: { number: nextVersion }, }), }) diff --git a/apps/sim/app/api/tools/confluence/space/route.ts b/apps/sim/app/api/tools/confluence/space/route.ts index bbdd9c597fc..8f723ea064c 100644 --- a/apps/sim/app/api/tools/confluence/space/route.ts +++ b/apps/sim/app/api/tools/confluence/space/route.ts @@ -200,8 +200,6 @@ export const PUT = withRouteHandler(async (request: NextRequest) => { return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) } - const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}` - if (!name && description === undefined) { return NextResponse.json( { error: 'At least one of name or description is required for update' }, @@ -209,39 +207,38 @@ export const PUT = withRouteHandler(async (request: NextRequest) => { ) } - const updateBody: Record = {} - - if (name) { - updateBody.name = name - } else { - const currentResponse = await fetch(url, { - headers: { - Accept: 'application/json', - Authorization: `Bearer ${accessToken}`, + const lookupUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}` + const lookupResponse = await fetch(lookupUrl, { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + if (!lookupResponse.ok) { + const errorText = await lookupResponse.text() + return NextResponse.json( + { + error: parseAtlassianErrorMessage( + lookupResponse.status, + lookupResponse.statusText, + errorText + ), }, - }) - if (!currentResponse.ok) { - const errorText = await currentResponse.text() - return NextResponse.json( - { - error: parseAtlassianErrorMessage( - currentResponse.status, - currentResponse.statusText, - errorText - ), - }, - { status: currentResponse.status } - ) - } - const currentSpace = await currentResponse.json() - updateBody.name = currentSpace.name + { status: lookupResponse.status } + ) } + const currentSpace = await lookupResponse.json() + const spaceKey = currentSpace.key + const updateBody: Record = { + name: name || currentSpace.name, + } if (description !== undefined) { - updateBody.description = { value: description, representation: 'plain' } + updateBody.description = { plain: { value: description, representation: 'plain' } } } - logger.info(`Updating space ${spaceId}`) + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/rest/api/space/${encodeURIComponent(spaceKey)}` + logger.info(`Updating space ${spaceKey}`) const response = await fetch(url, { method: 'PUT', @@ -315,9 +312,32 @@ export const DELETE = withRouteHandler(async (request: NextRequest) => { return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) } - const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}` + const lookupUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}` + const lookupResponse = await fetch(lookupUrl, { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + if (!lookupResponse.ok) { + const errorText = await lookupResponse.text() + return NextResponse.json( + { + error: parseAtlassianErrorMessage( + lookupResponse.status, + lookupResponse.statusText, + errorText + ), + }, + { status: lookupResponse.status } + ) + } + const currentSpace = await lookupResponse.json() + const spaceKey = currentSpace.key - logger.info(`Deleting space ${spaceId}`) + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/rest/api/space/${encodeURIComponent(spaceKey)}` + + logger.info(`Deleting space ${spaceKey}`) const response = await fetch(url, { method: 'DELETE', @@ -340,7 +360,26 @@ export const DELETE = withRouteHandler(async (request: NextRequest) => { ) } - return NextResponse.json({ spaceId, deleted: true }) + let longTask: { id?: string; statusLink?: string } = {} + try { + const text = await response.text() + if (text) { + const data = JSON.parse(text) + longTask = { + id: data?.id, + statusLink: data?.links?.status, + } + } + } catch { + // 204 No Content or non-JSON body — ignore + } + + return NextResponse.json({ + spaceId, + deleted: true, + longTaskId: longTask.id, + longTaskStatusLink: longTask.statusLink, + }) } catch (error) { logger.error('Error deleting Confluence space:', error) return NextResponse.json( diff --git a/apps/sim/app/api/tools/jira/issues/route.ts b/apps/sim/app/api/tools/jira/issues/route.ts index 897719c0528..4dc822313e6 100644 --- a/apps/sim/app/api/tools/jira/issues/route.ts +++ b/apps/sim/app/api/tools/jira/issues/route.ts @@ -41,6 +41,20 @@ export const POST = withRouteHandler(async (request: NextRequest) => { return NextResponse.json({ issues: [] }) } + const ISSUE_KEY_RE = /^[A-Za-z][A-Za-z0-9_]*-\d+$/ + const sanitizedKeys: string[] = [] + for (const k of issueKeys) { + if (typeof k !== 'string') continue + const trimmed = k.trim() + if (!ISSUE_KEY_RE.test(trimmed)) { + return NextResponse.json({ error: `Invalid Jira issue key: "${trimmed}"` }, { status: 400 }) + } + sanitizedKeys.push(trimmed) + } + if (sanitizedKeys.length === 0) { + return NextResponse.json({ issues: [] }) + } + const cloudId = providedCloudId || (await getJiraCloudId(domain!, accessToken!)) const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') @@ -49,11 +63,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => { } // Use search/jql endpoint (GET) with URL parameters - const jql = `issueKey in (${issueKeys.map((k: string) => k.trim()).join(',')})` + const jql = `issueKey in (${sanitizedKeys.join(',')})` const params = new URLSearchParams({ jql, fields: 'summary,status,assignee,updated,project', - maxResults: String(Math.min(issueKeys.length, 100)), + maxResults: String(Math.min(sanitizedKeys.length, 100)), }) const searchUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/search/jql?${params.toString()}` @@ -154,14 +168,13 @@ export const GET = withRouteHandler(async (request: NextRequest) => { const target = Math.min(all ? limit || SAFETY_CAP : 25, SAFETY_CAP) const projectKey = (projectId || manualProjectId || '').trim() - const escapeJql = (s: string) => s.replace(/"/g, '\\"') + const escapeJql = (s: string) => s.replace(/\\/g, '\\\\').replace(/"/g, '\\"') - const buildJql = (startAt: number) => { + const buildUrl = (token?: string) => { const jqlParts: string[] = [] - if (projectKey) jqlParts.push(`project = ${projectKey}`) + if (projectKey) jqlParts.push(`project = "${escapeJql(projectKey)}"`) if (query) { const q = escapeJql(query) - // Match by key prefix or summary text jqlParts.push(`(key ~ "${q}" OR summary ~ "${q}")`) } const jql = `${jqlParts.length ? `${jqlParts.join(' AND ')} ` : ''}ORDER BY updated DESC` @@ -170,20 +183,15 @@ export const GET = withRouteHandler(async (request: NextRequest) => { fields: 'summary,key,updated', maxResults: String(Math.min(PAGE_SIZE, target)), }) - if (startAt > 0) { - params.set('startAt', String(startAt)) - } - return { - url: `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/search/jql?${params.toString()}`, - } + if (token) params.set('nextPageToken', token) + return `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/search/jql?${params.toString()}` } - let startAt = 0 + let nextPageToken: string | undefined let collected: any[] = [] - let total = 0 do { - const { url: apiUrl } = buildJql(startAt) + const apiUrl = buildUrl(nextPageToken) const response = await fetch(apiUrl, { method: 'GET', headers: { @@ -209,10 +217,10 @@ export const GET = withRouteHandler(async (request: NextRequest) => { const page = await response.json() const issues = page.issues || [] - total = page.total || issues.length collected = collected.concat(issues) - startAt += PAGE_SIZE - } while (all && collected.length < Math.min(total, target)) + nextPageToken = page.nextPageToken + if (!nextPageToken || issues.length === 0) break + } while (all && collected.length < target) const issues = collected.slice(0, target).map((it: any) => ({ key: it.key, diff --git a/apps/sim/app/api/tools/jira/update/route.ts b/apps/sim/app/api/tools/jira/update/route.ts index 7945b2d29bc..77369ff773b 100644 --- a/apps/sim/app/api/tools/jira/update/route.ts +++ b/apps/sim/app/api/tools/jira/update/route.ts @@ -80,7 +80,8 @@ export const PUT = withRouteHandler(async (request: NextRequest) => { return NextResponse.json({ error: issueKeyValidation.error }, { status: 400 }) } - const notifyParam = notifyUsers === false ? '?notifyUsers=false' : '' + const notifyParam = + notifyUsers === false ? '?notifyUsers=false' : notifyUsers === true ? '?notifyUsers=true' : '' const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${issueKey}${notifyParam}` logger.info('Updating Jira issue at:', url) @@ -170,7 +171,8 @@ export const PUT = withRouteHandler(async (request: NextRequest) => { ) } - const responseData = response.status === 204 ? {} : await response.json() + const responseData = + response.status === 204 ? {} : await response.json().catch(() => ({}) as Record) logger.info('Successfully updated Jira issue:', issueKey) return NextResponse.json({ @@ -178,7 +180,7 @@ export const PUT = withRouteHandler(async (request: NextRequest) => { output: { ts: new Date().toISOString(), issueKey: responseData.key || issueKey, - summary: responseData.fields?.summary || 'Issue updated', + summary: responseData.fields?.summary || summaryValue || 'Issue updated', success: true, }, }) diff --git a/apps/sim/app/api/tools/jira/write/route.ts b/apps/sim/app/api/tools/jira/write/route.ts index be0bb063ef6..0e57cca73ec 100644 --- a/apps/sim/app/api/tools/jira/write/route.ts +++ b/apps/sim/app/api/tools/jira/write/route.ts @@ -91,7 +91,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => { } if (parent !== undefined && parent !== null && parent !== '') { - fields.parent = parent + if (typeof parent === 'string') { + fields.parent = /^\d+$/.test(parent) ? { id: parent } : { key: parent } + } else if (typeof parent === 'object') { + fields.parent = parent + } } if (priority !== undefined && priority !== null && priority !== '') { diff --git a/apps/sim/app/api/tools/jsm/customers/route.ts b/apps/sim/app/api/tools/jsm/customers/route.ts index 5924cada69c..456a1b92f49 100644 --- a/apps/sim/app/api/tools/jsm/customers/route.ts +++ b/apps/sim/app/api/tools/jsm/customers/route.ts @@ -28,7 +28,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => { start, limit, accountIds, - emails, } = body if (!domain) { @@ -46,6 +45,16 @@ export const POST = withRouteHandler(async (request: NextRequest) => { return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 }) } + if (body.emails !== undefined) { + return NextResponse.json( + { + error: + 'The `emails` parameter is no longer supported. Use `accountIds` (Atlassian account IDs) instead.', + }, + { status: 400 } + ) + } + const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken)) const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') @@ -60,33 +69,31 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const baseUrl = getJsmApiBaseUrl(cloudId) - const rawIds = accountIds || emails - const parsedAccountIds = rawIds - ? typeof rawIds === 'string' - ? rawIds - .split(',') - .map((id: string) => id.trim()) - .filter((id: string) => id) - : Array.isArray(rawIds) - ? rawIds - : [] - : [] - - const isAddOperation = parsedAccountIds.length > 0 - - if (isAddOperation) { + const splitCsv = (value: unknown): string[] => + value + ? typeof value === 'string' + ? value + .split(',') + .map((v: string) => v.trim()) + .filter((v: string) => v) + : Array.isArray(value) + ? (value as string[]) + : [] + : [] + + const parsedAccountIds = splitCsv(accountIds) + + if (parsedAccountIds.length > 0) { const url = `${baseUrl}/servicedesk/${serviceDeskId}/customer` - logger.info('Adding customers to:', url, { accountIds: parsedAccountIds }) - - const requestBody: Record = { + logger.info('Adding customers to:', url, { accountIds: parsedAccountIds, - } + }) const response = await fetch(url, { method: 'POST', headers: getJsmHeaders(accessToken), - body: JSON.stringify(requestBody), + body: JSON.stringify({ accountIds: parsedAccountIds }), }) if (!response.ok) { diff --git a/apps/sim/app/api/tools/jsm/organization/route.ts b/apps/sim/app/api/tools/jsm/organization/route.ts index 6fb3fe54f94..69f4bb40ad4 100644 --- a/apps/sim/app/api/tools/jsm/organization/route.ts +++ b/apps/sim/app/api/tools/jsm/organization/route.ts @@ -130,6 +130,14 @@ export const POST = withRouteHandler(async (request: NextRequest) => { return NextResponse.json({ error: organizationIdValidation.error }, { status: 400 }) } + const orgIdNumeric = Number.parseInt(String(organizationId).trim(), 10) + if (!Number.isFinite(orgIdNumeric) || orgIdNumeric <= 0) { + return NextResponse.json( + { error: 'organizationId must be a positive integer' }, + { status: 400 } + ) + } + const url = `${baseUrl}/servicedesk/${serviceDeskId}/organization` logger.info('Adding organization to service desk:', { serviceDeskId, organizationId }) @@ -137,7 +145,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const response = await fetch(url, { method: 'POST', headers: getJsmHeaders(accessToken), - body: JSON.stringify({ organizationId: Number.parseInt(organizationId, 10) }), + body: JSON.stringify({ organizationId: orgIdNumeric }), }) if (response.status === 204 || response.ok) { diff --git a/apps/sim/blocks/blocks/ashby.ts b/apps/sim/blocks/blocks/ashby.ts index 1113c6f6983..66cfe6186b6 100644 --- a/apps/sim/blocks/blocks/ashby.ts +++ b/apps/sim/blocks/blocks/ashby.ts @@ -151,6 +151,40 @@ export const AshbyBlock: BlockConfig = { }, mode: 'advanced', }, + { + id: 'website', + title: 'Website URL', + type: 'short-input', + placeholder: 'https://example.com', + condition: { field: 'operation', value: 'create_candidate' }, + mode: 'advanced', + }, + { + id: 'alternateEmail', + title: 'Alternate Email', + type: 'short-input', + placeholder: 'Additional email address', + condition: { field: 'operation', value: 'update_candidate' }, + mode: 'advanced', + }, + { + id: 'candidateCreatedAt', + title: 'Created At', + type: 'short-input', + placeholder: 'e.g. 2024-01-01T00:00:00Z', + condition: { field: 'operation', value: ['create_candidate', 'update_candidate'] }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: `Generate an ISO 8601 timestamp based on the user's description. +Examples: +- "last week" -> One week ago from today at 00:00:00Z +- "January 1st 2024" -> 2024-01-01T00:00:00Z +- "30 days ago" -> 30 days before today at 00:00:00Z +Output only the ISO 8601 timestamp string, nothing else.`, + generationType: 'timestamp', + }, + }, { id: 'updateName', title: 'Name', @@ -234,8 +268,11 @@ export const AshbyBlock: BlockConfig = { id: 'creditedToUserId', title: 'Credited To User ID', type: 'short-input', - placeholder: 'User UUID the application is credited to', - condition: { field: 'operation', value: 'create_application' }, + placeholder: 'User UUID credited as the source of this record', + condition: { + field: 'operation', + value: ['create_application', 'create_candidate', 'update_candidate'], + }, mode: 'advanced', }, { @@ -281,9 +318,33 @@ Output only the ISO 8601 timestamp string, nothing else.`, id: 'sendNotifications', title: 'Send Notifications', type: 'switch', + condition: { field: 'operation', value: ['create_note', 'update_candidate'] }, + mode: 'advanced', + }, + { + id: 'isPrivate', + title: 'Private Note', + type: 'switch', condition: { field: 'operation', value: 'create_note' }, mode: 'advanced', }, + { + id: 'noteCreatedAt', + title: 'Created At', + type: 'short-input', + placeholder: 'e.g. 2024-01-01T00:00:00Z', + condition: { field: 'operation', value: 'create_note' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: `Generate an ISO 8601 timestamp based on the user's description. +Examples: +- "yesterday" -> Yesterday at 00:00:00Z +- "January 1st 2024" -> 2024-01-01T00:00:00Z +Output only the ISO 8601 timestamp string, nothing else.`, + generationType: 'timestamp', + }, + }, { id: 'filterStatus', title: 'Status Filter', @@ -307,12 +368,30 @@ Output only the ISO 8601 timestamp string, nothing else.`, condition: { field: 'operation', value: 'list_applications' }, mode: 'advanced', }, + { + id: 'filterCandidateId', + title: 'Candidate ID Filter', + type: 'short-input', + placeholder: 'Filter by candidate UUID', + condition: { field: 'operation', value: 'list_applications' }, + mode: 'advanced', + }, { id: 'createdAfter', title: 'Created After', type: 'short-input', placeholder: 'e.g. 2024-01-01T00:00:00Z', - condition: { field: 'operation', value: 'list_applications' }, + condition: { + field: 'operation', + value: [ + 'list_applications', + 'list_candidates', + 'list_jobs', + 'list_offers', + 'list_openings', + 'list_interviews', + ], + }, mode: 'advanced', wandConfig: { enabled: true, @@ -322,6 +401,62 @@ Examples: - "January 1st 2024" -> 2024-01-01T00:00:00Z - "30 days ago" -> 30 days before today at 00:00:00Z - "start of this month" -> First day of current month at 00:00:00Z +Output only the ISO 8601 timestamp string, nothing else.`, + generationType: 'timestamp', + }, + }, + { + id: 'openedAfter', + title: 'Opened After', + type: 'short-input', + placeholder: 'e.g. 2024-01-01T00:00:00Z', + condition: { field: 'operation', value: 'list_jobs' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: `Generate an ISO 8601 timestamp based on the user's description. +Output only the ISO 8601 timestamp string, nothing else.`, + generationType: 'timestamp', + }, + }, + { + id: 'openedBefore', + title: 'Opened Before', + type: 'short-input', + placeholder: 'e.g. 2024-12-31T23:59:59Z', + condition: { field: 'operation', value: 'list_jobs' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: `Generate an ISO 8601 timestamp based on the user's description. +Output only the ISO 8601 timestamp string, nothing else.`, + generationType: 'timestamp', + }, + }, + { + id: 'closedAfter', + title: 'Closed After', + type: 'short-input', + placeholder: 'e.g. 2024-01-01T00:00:00Z', + condition: { field: 'operation', value: 'list_jobs' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: `Generate an ISO 8601 timestamp based on the user's description. +Output only the ISO 8601 timestamp string, nothing else.`, + generationType: 'timestamp', + }, + }, + { + id: 'closedBefore', + title: 'Closed Before', + type: 'short-input', + placeholder: 'e.g. 2024-12-31T23:59:59Z', + condition: { field: 'operation', value: 'list_jobs' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: `Generate an ISO 8601 timestamp based on the user's description. Output only the ISO 8601 timestamp string, nothing else.`, generationType: 'timestamp', }, @@ -358,6 +493,9 @@ Output only the ISO 8601 timestamp string, nothing else.`, 'list_users', 'list_interviews', 'list_candidate_tags', + 'list_locations', + 'list_departments', + 'list_custom_fields', ], }, mode: 'advanced', @@ -379,6 +517,9 @@ Output only the ISO 8601 timestamp string, nothing else.`, 'list_users', 'list_interviews', 'list_candidate_tags', + 'list_locations', + 'list_departments', + 'list_custom_fields', ], }, mode: 'advanced', @@ -388,7 +529,47 @@ Output only the ISO 8601 timestamp string, nothing else.`, title: 'Sync Token', type: 'short-input', placeholder: 'Sync token for incremental updates', - condition: { field: 'operation', value: 'list_candidate_tags' }, + condition: { + field: 'operation', + value: [ + 'list_candidate_tags', + 'list_locations', + 'list_departments', + 'list_custom_fields', + 'list_offers', + ], + }, + mode: 'advanced', + }, + { + id: 'includeLocationHierarchy', + title: 'Include Location Hierarchy', + type: 'switch', + condition: { field: 'operation', value: 'list_locations' }, + mode: 'advanced', + }, + { + id: 'offerApplicationId', + title: 'Application ID Filter', + type: 'short-input', + placeholder: 'Filter offers by application UUID', + condition: { field: 'operation', value: 'list_offers' }, + mode: 'advanced', + }, + { + id: 'alternateEmailAddresses', + title: 'Alternate Email Addresses', + type: 'long-input', + placeholder: 'Comma-separated or JSON array (e.g. ["a@x.com","b@x.com"])', + condition: { field: 'operation', value: 'create_candidate' }, + mode: 'advanced', + }, + { + id: 'socialLinks', + title: 'Social Links', + type: 'long-input', + placeholder: 'JSON array (e.g. [{"type":"Twitter","url":"https://twitter.com/x"}])', + condition: { field: 'operation', value: 'update_candidate' }, mode: 'advanced', }, { @@ -397,20 +578,58 @@ Output only the ISO 8601 timestamp string, nothing else.`, type: 'switch', condition: { field: 'operation', - value: ['list_candidate_tags', 'list_archive_reasons'], + value: [ + 'list_candidate_tags', + 'list_archive_reasons', + 'list_sources', + 'list_departments', + 'list_custom_fields', + 'list_locations', + ], }, mode: 'advanced', }, { - id: 'expandApplicationFormDefinition', - title: 'Include Application Form Definition', + id: 'includeDeactivated', + title: 'Include Deactivated', type: 'switch', - condition: { field: 'operation', value: 'get_job_posting' }, + condition: { field: 'operation', value: 'list_users' }, mode: 'advanced', }, { - id: 'expandSurveyFormDefinitions', - title: 'Include Survey Form Definitions', + id: 'jobBoardId', + title: 'Job Board ID', + type: 'short-input', + placeholder: 'Optional job board UUID (defaults to external)', + condition: { field: 'operation', value: ['get_job_posting', 'list_job_postings'] }, + mode: 'advanced', + }, + { + id: 'postingLocation', + title: 'Location Filter', + type: 'short-input', + placeholder: 'Filter by location name (case sensitive)', + condition: { field: 'operation', value: 'list_job_postings' }, + mode: 'advanced', + }, + { + id: 'postingDepartment', + title: 'Department Filter', + type: 'short-input', + placeholder: 'Filter by department name (case sensitive)', + condition: { field: 'operation', value: 'list_job_postings' }, + mode: 'advanced', + }, + { + id: 'listedOnly', + title: 'Listed Postings Only', + type: 'switch', + condition: { field: 'operation', value: 'list_job_postings' }, + mode: 'advanced', + }, + { + id: 'expandJob', + title: 'Include Job', type: 'switch', condition: { field: 'operation', value: 'get_job_posting' }, mode: 'advanced', @@ -501,6 +720,9 @@ Output only the ISO 8601 timestamp string, nothing else.`, if (params.searchEmail) result.email = params.searchEmail if (params.filterStatus) result.status = params.filterStatus if (params.filterJobId) result.jobId = params.filterJobId + if (params.operation === 'list_applications' && params.filterCandidateId) { + result.candidateId = params.filterCandidateId + } if (params.jobStatus) result.status = params.jobStatus if (params.sendNotifications === 'true' || params.sendNotifications === true) { result.sendNotifications = true @@ -508,21 +730,51 @@ Output only the ISO 8601 timestamp string, nothing else.`, if (params.includeArchived === 'true' || params.includeArchived === true) { result.includeArchived = true } + if (params.includeDeactivated === 'true' || params.includeDeactivated === true) { + result.includeDeactivated = true + } + if (params.isPrivate === 'true' || params.isPrivate === true) { + result.isPrivate = true + } + if (params.listedOnly === 'true' || params.listedOnly === true) { + result.listedOnly = true + } + if (params.expandJob === 'true' || params.expandJob === true) { + result.expandJob = true + } + if (params.operation === 'create_application' && params.appCandidateId) { + result.candidateId = params.appCandidateId + } + if (params.operation === 'create_application' && params.appCreatedAt) { + result.createdAt = params.appCreatedAt + } if ( - params.expandApplicationFormDefinition === 'true' || - params.expandApplicationFormDefinition === true + (params.operation === 'create_candidate' || params.operation === 'update_candidate') && + params.candidateCreatedAt ) { - result.expandApplicationFormDefinition = true + result.createdAt = params.candidateCreatedAt } + if (params.operation === 'create_note' && params.noteCreatedAt) { + result.createdAt = params.noteCreatedAt + } + if (params.updateName) result.name = params.updateName + if (params.website) result.website = params.website + if (params.alternateEmail) result.alternateEmail = params.alternateEmail + if (params.postingLocation) result.location = params.postingLocation + if (params.postingDepartment) result.department = params.postingDepartment if ( - params.expandSurveyFormDefinitions === 'true' || - params.expandSurveyFormDefinitions === true + params.includeLocationHierarchy === 'true' || + params.includeLocationHierarchy === true ) { - result.expandSurveyFormDefinitions = true + result.includeLocationHierarchy = true } - if (params.appCandidateId) result.candidateId = params.appCandidateId - if (params.appCreatedAt) result.createdAt = params.appCreatedAt - if (params.updateName) result.name = params.updateName + if (params.operation === 'list_offers' && params.offerApplicationId) { + result.applicationId = params.offerApplicationId + } + if (params.alternateEmailAddresses) { + result.alternateEmailAddresses = params.alternateEmailAddresses + } + if (params.socialLinks) result.socialLinks = params.socialLinks return result }, }, @@ -554,24 +806,51 @@ Output only the ISO 8601 timestamp string, nothing else.`, sendNotifications: { type: 'boolean', description: 'Send notifications' }, filterStatus: { type: 'string', description: 'Application status filter' }, filterJobId: { type: 'string', description: 'Job UUID filter' }, + filterCandidateId: { type: 'string', description: 'Candidate UUID filter' }, createdAfter: { type: 'string', description: 'Filter by creation date' }, + openedAfter: { type: 'string', description: 'Filter jobs opened after this timestamp' }, + openedBefore: { type: 'string', description: 'Filter jobs opened before this timestamp' }, + closedAfter: { type: 'string', description: 'Filter jobs closed after this timestamp' }, + closedBefore: { type: 'string', description: 'Filter jobs closed before this timestamp' }, jobStatus: { type: 'string', description: 'Job status filter' }, cursor: { type: 'string', description: 'Pagination cursor' }, perPage: { type: 'number', description: 'Results per page' }, syncToken: { type: 'string', description: 'Sync token for incremental updates' }, includeArchived: { type: 'boolean', description: 'Include archived records' }, - expandApplicationFormDefinition: { - type: 'boolean', - description: 'Include application form definition in job posting', - }, - expandSurveyFormDefinitions: { + includeDeactivated: { type: 'boolean', description: 'Include deactivated users' }, + website: { type: 'string', description: 'Personal website URL for new candidate' }, + alternateEmail: { type: 'string', description: 'Additional email to add to candidate' }, + candidateCreatedAt: { type: 'string', description: 'Candidate creation timestamp override' }, + noteCreatedAt: { type: 'string', description: 'Note creation timestamp override' }, + isPrivate: { type: 'boolean', description: 'Whether the note is private' }, + postingLocation: { type: 'string', description: 'Filter job postings by location name' }, + postingDepartment: { type: 'string', description: 'Filter job postings by department name' }, + listedOnly: { type: 'boolean', description: 'Only return publicly listed job postings' }, + jobBoardId: { type: 'string', description: 'Job board UUID for job posting lookup' }, + expandJob: { type: 'boolean', - description: 'Include survey form definitions in job posting', + description: 'Include the related job object in job posting response', }, tagId: { type: 'string', description: 'Tag UUID' }, offerId: { type: 'string', description: 'Offer UUID' }, jobPostingId: { type: 'string', description: 'Job posting UUID' }, archiveReasonId: { type: 'string', description: 'Archive reason UUID' }, + includeLocationHierarchy: { + type: 'boolean', + description: 'Include hierarchical location data when listing locations', + }, + offerApplicationId: { + type: 'string', + description: 'Application UUID filter for list_offers', + }, + alternateEmailAddresses: { + type: 'string', + description: 'Alternate email addresses (comma-separated or JSON array)', + }, + socialLinks: { + type: 'string', + description: 'Social links as JSON array', + }, }, outputs: { @@ -583,7 +862,7 @@ Output only the ISO 8601 timestamp string, nothing else.`, jobs: { type: 'json', description: - 'List of jobs (id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds[], customFields[], jobPostingIds[], customRequisitionId, brandId, hiringTeam[], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings[] with latestVersion, compensation with compensationTiers[])', + 'List of jobs (id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds[], customFields[], jobPostingIds[], customRequisitionId, brandId, hiringTeam[], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings[] with latestVersion)', }, applications: { type: 'json', @@ -636,7 +915,7 @@ Output only the ISO 8601 timestamp string, nothing else.`, users: { type: 'json', description: - 'List of users (id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId)', + 'List of users (id, firstName, lastName, email, globalRole, isEnabled, updatedAt)', }, interviewSchedules: { type: 'json', @@ -654,12 +933,12 @@ Output only the ISO 8601 timestamp string, nothing else.`, candidate: { type: 'json', description: - 'Candidate details (id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses[], phoneNumbers[], socialLinks[], customFields[], source, creditedToUser, createdAt, updatedAt)', + 'Candidate summary (id, name, primaryEmailAddress, primaryPhoneNumber). For full candidate fields use the candidates list output or the get/create/update candidate operations.', }, job: { type: 'json', description: - 'Job details (id, title, status, employmentType, locationId, departmentId, hiringTeam[], author, location, openings[], compensation, createdAt, updatedAt)', + 'Job details (id, title, status, employmentType, locationId, departmentId, hiringTeam[], author, location, openings[], createdAt, updatedAt)', }, application: { type: 'json', @@ -674,12 +953,12 @@ Output only the ISO 8601 timestamp string, nothing else.`, jobPosting: { type: 'json', description: - 'Job posting details (id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy[], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt)', + 'Job posting details (id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy[], jobId, locationName, locationIds, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt, job [included when expandJob=true])', }, content: { type: 'string', description: 'Note content' }, author: { type: 'json', - description: 'Note author (id, firstName, lastName, email, globalRole, isEnabled)', + description: 'Note author (id, firstName, lastName, email)', }, isPrivate: { type: 'boolean', description: 'Whether the note is private' }, createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' }, diff --git a/apps/sim/blocks/blocks/google_drive.ts b/apps/sim/blocks/blocks/google_drive.ts index 6feff2d80f9..495423beb22 100644 --- a/apps/sim/blocks/blocks/google_drive.ts +++ b/apps/sim/blocks/blocks/google_drive.ts @@ -27,14 +27,18 @@ export const GoogleDriveBlock: BlockConfig = { type: 'dropdown', options: [ { label: 'List Files', id: 'list' }, + { label: 'Search Files', id: 'search' }, { label: 'Get File Info', id: 'get_file' }, + { label: 'Get File Content', id: 'get_content' }, { label: 'Create Folder', id: 'create_folder' }, { label: 'Create File', id: 'create_file' }, { label: 'Upload File', id: 'upload' }, { label: 'Download File', id: 'download' }, { label: 'Copy File', id: 'copy' }, + { label: 'Move File', id: 'move' }, { label: 'Update File', id: 'update' }, { label: 'Move to Trash', id: 'trash' }, + { label: 'Restore from Trash', id: 'untrash' }, { label: 'Delete Permanently', id: 'delete' }, { label: 'Share File', id: 'share' }, { label: 'Remove Sharing', id: 'unshare' }, @@ -281,7 +285,7 @@ Return ONLY the query string - no explanations, no quotes around the whole thing id: 'pageSize', title: 'Results Per Page', type: 'short-input', - placeholder: 'Number of results (default: 100, max: 1000)', + placeholder: 'Number of results (default: 100, max: 100)', condition: { field: 'operation', value: 'list' }, }, // Download File Fields - File Selector (basic mode) @@ -721,6 +725,198 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr condition: { field: 'operation', value: 'list_permissions' }, required: true, }, + // Get File Content Fields + { + id: 'getContentFileSelector', + title: 'Select File', + type: 'file-selector', + canonicalParamId: 'getContentFileId', + serviceId: 'google-drive', + selectorKey: 'google.drive', + requiredScopes: getScopesForService('google-drive'), + placeholder: 'Select a file to get content from', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'get_content' }, + required: true, + }, + { + id: 'getContentManualFileId', + title: 'File ID', + type: 'short-input', + canonicalParamId: 'getContentFileId', + placeholder: 'Enter file ID', + mode: 'advanced', + condition: { field: 'operation', value: 'get_content' }, + required: true, + }, + { + id: 'getContentExportMimeType', + title: 'Export Format', + type: 'dropdown', + options: [ + { label: 'Auto (best format for file type)', id: 'auto' }, + { label: 'Plain Text (text/plain)', id: 'text/plain' }, + { label: 'HTML (text/html)', id: 'text/html' }, + { label: 'PDF (application/pdf)', id: 'application/pdf' }, + { + label: 'DOCX (MS Word)', + id: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }, + { + label: 'XLSX (MS Excel)', + id: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }, + { + label: 'PPTX (MS PowerPoint)', + id: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + }, + { label: 'CSV (text/csv)', id: 'text/csv' }, + ], + value: () => 'auto', + placeholder: 'Export format for Google Workspace files', + condition: { field: 'operation', value: 'get_content' }, + }, + { + id: 'getContentIncludeRevisions', + title: 'Include Revisions', + type: 'dropdown', + canonicalParamId: 'includeRevisions', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes (full revision history)', id: 'true' }, + ], + value: () => 'false', + placeholder: 'Include revision history', + mode: 'advanced', + condition: { field: 'operation', value: 'get_content' }, + }, + // Move File Fields + { + id: 'moveFileSelector', + title: 'Select File to Move', + type: 'file-selector', + canonicalParamId: 'moveFileId', + serviceId: 'google-drive', + selectorKey: 'google.drive', + requiredScopes: getScopesForService('google-drive'), + placeholder: 'Select a file to move', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'move' }, + required: true, + }, + { + id: 'moveManualFileId', + title: 'File ID', + type: 'short-input', + canonicalParamId: 'moveFileId', + placeholder: 'Enter file ID to move', + mode: 'advanced', + condition: { field: 'operation', value: 'move' }, + required: true, + }, + { + id: 'moveDestFolderSelector', + title: 'Destination Folder', + type: 'file-selector', + canonicalParamId: 'moveDestFolderId', + serviceId: 'google-drive', + selectorKey: 'google.drive', + requiredScopes: getScopesForService('google-drive'), + mimeType: 'application/vnd.google-apps.folder', + placeholder: 'Select destination folder', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'move' }, + required: true, + }, + { + id: 'moveManualDestFolderId', + title: 'Destination Folder ID', + type: 'short-input', + canonicalParamId: 'moveDestFolderId', + placeholder: 'Enter destination folder ID', + mode: 'advanced', + condition: { field: 'operation', value: 'move' }, + required: true, + }, + { + id: 'moveRemoveFromCurrent', + title: 'Remove from Current Folder', + type: 'dropdown', + canonicalParamId: 'removeFromCurrent', + options: [ + { label: 'Yes (default)', id: 'true' }, + { label: 'No (add to destination, keep in current)', id: 'false' }, + ], + placeholder: 'Remove from current folder', + mode: 'advanced', + condition: { field: 'operation', value: 'move' }, + }, + // Search Files Fields + { + id: 'searchQuery', + title: 'Search Query', + type: 'long-input', + placeholder: + "Drive query syntax (e.g., fullText contains 'budget' and mimeType = 'application/pdf')", + condition: { field: 'operation', value: 'search' }, + required: true, + wandConfig: { + enabled: true, + prompt: `Generate a Google Drive search query based on the user's description. +Use Google Drive query syntax: +- name contains 'term' - search by filename +- fullText contains 'term' - search file contents +- mimeType = 'type' - filter by file type (e.g., 'application/pdf', 'application/vnd.google-apps.document') +- modifiedTime > 'YYYY-MM-DDTHH:MM:SS' - filter by date +- 'email' in owners - filter by owner +- trashed = false - exclude trashed files +- starred = true - only starred files +- 'folderId' in parents - files in a specific folder + +Combine with 'and' / 'or' / 'not'. Example: +- "PDFs about budget modified this year" -> mimeType = 'application/pdf' and fullText contains 'budget' and modifiedTime > '2024-01-01T00:00:00' +- "starred Google Docs" -> mimeType = 'application/vnd.google-apps.document' and starred = true + +Return ONLY the query string - no explanations, no quotes around the whole thing, no extra text.`, + placeholder: 'Describe the files you want to find...', + }, + }, + { + id: 'searchPageSize', + title: 'Results Per Page', + type: 'short-input', + placeholder: 'Number of results (default: 100, max: 100)', + mode: 'advanced', + condition: { field: 'operation', value: 'search' }, + }, + // Untrash File Fields + { + id: 'untrashFileSelector', + title: 'Select File to Restore', + type: 'file-selector', + canonicalParamId: 'untrashFileId', + serviceId: 'google-drive', + selectorKey: 'google.drive', + requiredScopes: getScopesForService('google-drive'), + placeholder: 'Select a file to restore from trash', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'untrash' }, + required: true, + }, + { + id: 'untrashManualFileId', + title: 'File ID', + type: 'short-input', + canonicalParamId: 'untrashFileId', + placeholder: 'Enter file ID to restore', + mode: 'advanced', + condition: { field: 'operation', value: 'untrash' }, + required: true, + }, // Get Drive Info has no additional fields (just needs credential) ...getTrigger('google_drive_poller').subBlocks, ], @@ -728,12 +924,16 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr access: [ 'google_drive_list', 'google_drive_get_file', + 'google_drive_get_content', 'google_drive_create_folder', 'google_drive_upload', 'google_drive_download', 'google_drive_copy', + 'google_drive_move', + 'google_drive_search', 'google_drive_update', 'google_drive_trash', + 'google_drive_untrash', 'google_drive_delete', 'google_drive_share', 'google_drive_unshare', @@ -745,8 +945,12 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr switch (params.operation) { case 'list': return 'google_drive_list' + case 'search': + return 'google_drive_search' case 'get_file': return 'google_drive_get_file' + case 'get_content': + return 'google_drive_get_content' case 'create_folder': return 'google_drive_create_folder' case 'create_file': @@ -756,10 +960,14 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr return 'google_drive_download' case 'copy': return 'google_drive_copy' + case 'move': + return 'google_drive_move' case 'update': return 'google_drive_update' case 'trash': return 'google_drive_trash' + case 'untrash': + return 'google_drive_untrash' case 'delete': return 'google_drive_delete' case 'share': @@ -782,12 +990,16 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr createFolderParentId, listFolderId, copyDestFolderId, + moveDestFolderId, // File canonical params (per-operation) downloadFileId, getFileId, + getContentFileId, copyFileId, + moveFileId, updateFileId, trashFileId, + untrashFileId, deleteFileId, shareFileId, unshareFileId, @@ -798,6 +1010,13 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr shareType, starred, sendNotification, + removeFromCurrent, + includeRevisions, + pageSize, + query, + searchQuery, + searchPageSize, + getContentExportMimeType, ...rest } = params @@ -828,15 +1047,24 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr case 'get_file': effectiveFileId = getFileId?.trim() || undefined break + case 'get_content': + effectiveFileId = getContentFileId?.trim() || undefined + break case 'copy': effectiveFileId = copyFileId?.trim() || undefined break + case 'move': + effectiveFileId = moveFileId?.trim() || undefined + break case 'update': effectiveFileId = updateFileId?.trim() || undefined break case 'trash': effectiveFileId = trashFileId?.trim() || undefined break + case 'untrash': + effectiveFileId = untrashFileId?.trim() || undefined + break case 'delete': effectiveFileId = deleteFileId?.trim() || undefined break @@ -851,9 +1079,13 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr break } - // Resolve destinationFolderId for copy operation - const effectiveDestinationFolderId = - params.operation === 'copy' ? copyDestFolderId?.trim() || undefined : undefined + // Resolve destinationFolderId for copy/move operations + let effectiveDestinationFolderId: string | undefined + if (params.operation === 'copy') { + effectiveDestinationFolderId = copyDestFolderId?.trim() || undefined + } else if (params.operation === 'move') { + effectiveDestinationFolderId = moveDestFolderId?.trim() || undefined + } // Convert starred dropdown to boolean const starredValue = starred === 'true' ? true : starred === 'false' ? false : undefined @@ -862,17 +1094,35 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr const sendNotificationValue = sendNotification === 'true' ? true : sendNotification === 'false' ? false : undefined + // Convert removeFromCurrent dropdown to boolean + const removeFromCurrentValue = + removeFromCurrent === 'true' ? true : removeFromCurrent === 'false' ? false : undefined + + // Convert includeRevisions dropdown to boolean + const includeRevisionsValue = + includeRevisions === 'true' ? true : includeRevisions === 'false' ? false : undefined + + const effectivePageSize = params.operation === 'search' ? searchPageSize : pageSize + const effectiveQuery = params.operation === 'search' ? searchQuery : query + const effectiveMimeType = + params.operation === 'get_content' ? getContentExportMimeType : mimeType + return { oauthCredential, folderId: effectiveFolderId, fileId: effectiveFileId, destinationFolderId: effectiveDestinationFolderId, file: normalizedFile, - pageSize: rest.pageSize ? Number.parseInt(rest.pageSize as string, 10) : undefined, - mimeType: mimeType === 'auto' ? undefined : mimeType, + pageSize: effectivePageSize + ? Number.parseInt(effectivePageSize as string, 10) + : undefined, + query: effectiveQuery, + mimeType: effectiveMimeType === 'auto' ? undefined : effectiveMimeType, type: shareType, // Map shareType to type for share tool starred: starredValue, sendNotification: sendNotificationValue, + removeFromCurrent: removeFromCurrentValue, + includeRevisions: includeRevisionsValue, transferOwnership: rest.role === 'owner' ? true : undefined, ...rest, } @@ -887,16 +1137,27 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr createFolderParentId: { type: 'string', description: 'Parent folder for create folder' }, listFolderId: { type: 'string', description: 'Folder to list files from' }, copyDestFolderId: { type: 'string', description: 'Destination folder for copy' }, + moveDestFolderId: { type: 'string', description: 'Destination folder for move' }, // File canonical params (per-operation) downloadFileId: { type: 'string', description: 'File to download' }, getFileId: { type: 'string', description: 'File to get info for' }, + getContentFileId: { type: 'string', description: 'File to get content from' }, copyFileId: { type: 'string', description: 'File to copy' }, + moveFileId: { type: 'string', description: 'File to move' }, updateFileId: { type: 'string', description: 'File to update' }, trashFileId: { type: 'string', description: 'File to trash' }, + untrashFileId: { type: 'string', description: 'File to restore from trash' }, deleteFileId: { type: 'string', description: 'File to delete' }, shareFileId: { type: 'string', description: 'File to share' }, unshareFileId: { type: 'string', description: 'File to unshare' }, listPermissionsFileId: { type: 'string', description: 'File to list permissions for' }, + // Move operation inputs + removeFromCurrent: { + type: 'string', + description: 'Whether to remove from current folder when moving', + }, + // Get content operation inputs + includeRevisions: { type: 'string', description: 'Whether to include revision history' }, // Upload and Create inputs fileName: { type: 'string', description: 'File or folder name' }, file: { type: 'json', description: 'File to upload (UserFile object)' }, diff --git a/apps/sim/blocks/blocks/jira.ts b/apps/sim/blocks/blocks/jira.ts index e1d8e6d206b..de5c671adbc 100644 --- a/apps/sim/blocks/blocks/jira.ts +++ b/apps/sim/blocks/blocks/jira.ts @@ -27,6 +27,7 @@ export const JiraBlock: BlockConfig = { type: 'dropdown', options: [ { label: 'Read Issue', id: 'read' }, + { label: 'Read Bulk Issues', id: 'read-bulk' }, { label: 'Update Issue', id: 'update' }, { label: 'Write Issue', id: 'write' }, { label: 'Delete Issue', id: 'delete' }, @@ -270,6 +271,7 @@ Return ONLY the description text - no explanations.`, placeholder: 'Parent issue key for subtasks (e.g., PROJ-123)', dependsOn: ['projectId'], condition: { field: 'operation', value: 'write' }, + mode: 'advanced', }, // Write/Update Issue additional fields { @@ -320,6 +322,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n placeholder: 'Reporter account ID', dependsOn: ['projectId'], condition: { field: 'operation', value: 'write' }, + mode: 'advanced', }, { id: 'environment', @@ -327,6 +330,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n type: 'long-input', placeholder: 'Environment information (e.g., Production, Staging)', condition: { field: 'operation', value: ['write', 'update'] }, + mode: 'advanced', }, { id: 'customFieldId', @@ -334,6 +338,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n type: 'short-input', placeholder: 'e.g., customfield_10001 or 10001', condition: { field: 'operation', value: ['write', 'update'] }, + mode: 'advanced', }, { id: 'customFieldValue', @@ -341,6 +346,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n type: 'short-input', placeholder: 'Value for the custom field', condition: { field: 'operation', value: ['write', 'update'] }, + mode: 'advanced', }, { id: 'components', @@ -348,6 +354,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n type: 'short-input', placeholder: 'Comma-separated component names', condition: { field: 'operation', value: ['write', 'update'] }, + mode: 'advanced', }, { id: 'fixVersions', @@ -355,6 +362,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n type: 'short-input', placeholder: 'Comma-separated fix version names', condition: { field: 'operation', value: ['write', 'update'] }, + mode: 'advanced', }, { id: 'notifyUsers', @@ -366,6 +374,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n ], value: () => 'true', condition: { field: 'operation', value: 'update' }, + mode: 'advanced', }, // Delete Issue fields { @@ -378,6 +387,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n ], value: () => 'false', condition: { field: 'operation', value: 'delete' }, + mode: 'advanced', }, // Assign Issue fields { @@ -421,6 +431,7 @@ Return ONLY the comment text - no explanations.`, type: 'short-input', placeholder: 'Resolution name (e.g., "Fixed", "Won\'t Fix")', condition: { field: 'operation', value: 'transition' }, + mode: 'advanced', }, // Search Issues fields { @@ -453,6 +464,7 @@ Return ONLY the JQL query - no explanations or markdown formatting.`, type: 'short-input', placeholder: 'Cursor token for next page (omit for first page)', condition: { field: 'operation', value: 'search' }, + mode: 'advanced', }, { id: 'startAt', @@ -460,6 +472,7 @@ Return ONLY the JQL query - no explanations or markdown formatting.`, type: 'short-input', placeholder: 'Pagination start index (default: 0)', condition: { field: 'operation', value: ['get_comments', 'get_worklogs'] }, + mode: 'advanced', }, { id: 'maxResults', @@ -467,6 +480,7 @@ Return ONLY the JQL query - no explanations or markdown formatting.`, type: 'short-input', placeholder: 'Maximum results to return (default: 50)', condition: { field: 'operation', value: ['search', 'get_comments', 'get_worklogs'] }, + mode: 'advanced', }, { id: 'fields', @@ -474,6 +488,7 @@ Return ONLY the JQL query - no explanations or markdown formatting.`, type: 'short-input', placeholder: 'Comma-separated fields to return (e.g., key,summary,status)', condition: { field: 'operation', value: 'search' }, + mode: 'advanced', }, // Comment fields { @@ -629,6 +644,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, type: 'long-input', placeholder: 'Add optional comment for the link', condition: { field: 'operation', value: 'create_link' }, + mode: 'advanced', wandConfig: { enabled: true, prompt: `Generate a comment for a Jira issue link based on the user's description. @@ -657,6 +673,7 @@ Return ONLY the comment text - no explanations.`, type: 'short-input', placeholder: 'Enter account ID for specific user', condition: { field: 'operation', value: 'get_users' }, + mode: 'advanced', }, { id: 'usersStartAt', @@ -664,6 +681,7 @@ Return ONLY the comment text - no explanations.`, type: 'short-input', placeholder: 'Pagination start index (default: 0)', condition: { field: 'operation', value: 'get_users' }, + mode: 'advanced', }, { id: 'usersMaxResults', @@ -671,6 +689,7 @@ Return ONLY the comment text - no explanations.`, type: 'short-input', placeholder: 'Maximum users to return (default: 50)', condition: { field: 'operation', value: 'get_users' }, + mode: 'advanced', }, // Search Users fields { @@ -744,16 +763,8 @@ Return ONLY the comment text - no explanations.`, ], config: { tool: (params) => { - // Use canonical param IDs (raw subBlock IDs are deleted after serialization) - const effectiveProjectId = params.projectId ? String(params.projectId).trim() : '' - const effectiveIssueKey = params.issueKey ? String(params.issueKey).trim() : '' - switch (params.operation) { case 'read': - // If a project is selected but no issue is chosen, route to bulk read - if (effectiveProjectId && !effectiveIssueKey) { - return 'jira_bulk_read' - } return 'jira_retrieve' case 'update': return 'jira_update' @@ -877,7 +888,12 @@ Return ONLY the comment text - no explanations.`, environment: params.environment || undefined, customFieldId: params.customFieldId || undefined, customFieldValue: params.customFieldValue || undefined, - notifyUsers: params.notifyUsers === 'false' ? false : undefined, + notifyUsers: + params.notifyUsers === 'false' + ? false + : params.notifyUsers === 'true' + ? true + : undefined, } return { ...baseParams, @@ -989,12 +1005,20 @@ Return ONLY the comment text - no explanations.`, } } case 'add_worklog': { + const rawTime = params.timeSpentSeconds + const parsedTime = + rawTime !== undefined && rawTime !== null && String(rawTime).trim() !== '' + ? Number(String(rawTime).trim()) + : undefined + if (parsedTime !== undefined && (!Number.isFinite(parsedTime) || parsedTime <= 0)) { + throw new Error( + 'Time Spent (seconds) must be a positive number of seconds (e.g., 3600 for 1 hour)' + ) + } return { ...baseParams, issueKey: effectiveIssueKey, - timeSpentSeconds: params.timeSpentSeconds - ? Number.parseInt(params.timeSpentSeconds) - : undefined, + timeSpentSeconds: parsedTime, comment: params.worklogComment, started: params.started, } @@ -1012,9 +1036,17 @@ Return ONLY the comment text - no explanations.`, ...baseParams, issueKey: effectiveIssueKey, worklogId: params.worklogId, - timeSpentSeconds: params.timeSpentSecondsUpdate - ? Number.parseInt(params.timeSpentSecondsUpdate) - : undefined, + timeSpentSeconds: (() => { + const raw = params.timeSpentSecondsUpdate + if (raw === undefined || raw === null || String(raw).trim() === '') return undefined + const n = Number(String(raw).trim()) + if (!Number.isFinite(n) || n <= 0) { + throw new Error( + 'Time Spent (seconds) must be a positive number of seconds (e.g., 3600 for 1 hour)' + ) + } + return n + })(), comment: params.worklogComment, started: params.started, } diff --git a/apps/sim/blocks/blocks/jira_service_management.ts b/apps/sim/blocks/blocks/jira_service_management.ts index 1e7c94f825f..342cccc0770 100644 --- a/apps/sim/blocks/blocks/jira_service_management.ts +++ b/apps/sim/blocks/blocks/jira_service_management.ts @@ -798,14 +798,13 @@ Return ONLY the comment text - no explanations.`, if (!params.serviceDeskId) { throw new Error('Service Desk ID is required') } - if (!params.accountIds && !params.emails) { - throw new Error('Account IDs or emails are required') + if (!params.accountIds) { + throw new Error('Account IDs are required') } return { ...baseParams, serviceDeskId: params.serviceDeskId, accountIds: params.accountIds, - emails: params.emails, } } case 'get_organizations': @@ -1238,7 +1237,8 @@ Return ONLY the comment text - no explanations.`, status: { type: 'string', description: 'Form status (open, submitted, locked)' }, answers: { type: 'json', - description: 'Form answers as key-value pairs (question ID to answer)', + description: + 'Array of simplified form answers, each with label, answer, fieldKey, and choice', }, deleted: { type: 'boolean', description: 'Whether the form was successfully deleted' }, visibility: { diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index 508c54c7a0e..c303faa1c5a 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -494,6 +494,18 @@ Do not include any explanations, markdown formatting, or other text outside the value: 'list_users', }, }, + // Pagination cursor (shared across list_channels, list_members, list_users) + { + id: 'paginationCursor', + title: 'Pagination Cursor', + type: 'short-input', + placeholder: 'next_cursor from a previous response', + condition: { + field: 'operation', + value: ['list_channels', 'list_members', 'list_users'], + }, + mode: 'advanced', + }, // Get User specific fields { id: 'userId', @@ -1331,6 +1343,9 @@ Do not include any explanations, markdown formatting, or other text outside the viewHash, publishUserId, viewPayload, + fileId, + fileName, + paginationCursor, ...rest } = params @@ -1421,17 +1436,26 @@ Do not include any explanations, markdown formatting, or other text outside the baseParams.includePrivate = includePrivate !== 'false' baseParams.excludeArchived = true baseParams.limit = channelLimit ? Number.parseInt(channelLimit, 10) : 100 + if (paginationCursor) { + baseParams.cursor = String(paginationCursor).trim() + } break } case 'list_members': { baseParams.limit = memberLimit ? Number.parseInt(memberLimit, 10) : 100 + if (paginationCursor) { + baseParams.cursor = String(paginationCursor).trim() + } break } case 'list_users': { baseParams.includeDeleted = includeDeleted === 'true' baseParams.limit = userLimit ? Number.parseInt(userLimit, 10) : 100 + if (paginationCursor) { + baseParams.cursor = String(paginationCursor).trim() + } break } @@ -1440,8 +1464,6 @@ Do not include any explanations, markdown formatting, or other text outside the break case 'download': { - const fileId = (rest as any).fileId - const fileName = (rest as any).fileName baseParams.fileId = fileId if (fileName) { baseParams.fileName = fileName @@ -1561,18 +1583,24 @@ Do not include any explanations, markdown formatting, or other text outside the baseParams.view = viewPayload break - case 'update_view': - if (viewId) { - baseParams.viewId = viewId + case 'update_view': { + const trimmedViewId = viewId ? String(viewId).trim() : '' + const trimmedExternalId = viewExternalId ? String(viewExternalId).trim() : '' + if (!trimmedViewId && !trimmedExternalId) { + throw new Error('update_view requires either View ID or External ID') } - if (viewExternalId) { - baseParams.externalId = viewExternalId + if (trimmedViewId) { + baseParams.viewId = trimmedViewId + } + if (trimmedExternalId) { + baseParams.externalId = trimmedExternalId } if (viewHash) { baseParams.hash = viewHash } baseParams.view = viewPayload break + } case 'push_view': baseParams.triggerId = viewTriggerId @@ -1630,6 +1658,11 @@ Do not include any explanations, markdown formatting, or other text outside the // List Users inputs includeDeleted: { type: 'string', description: 'Include deactivated users (true/false)' }, userLimit: { type: 'string', description: 'Maximum number of users to return' }, + // Shared pagination input + paginationCursor: { + type: 'string', + description: 'Pagination cursor (next_cursor) for list_channels/list_members/list_users', + }, // Ephemeral message inputs ephemeralUser: { type: 'string', description: 'User ID who will see the ephemeral message' }, blocks: { type: 'json', description: 'Block Kit layout blocks as a JSON array' }, @@ -1779,6 +1812,10 @@ Do not include any explanations, markdown formatting, or other text outside the type: 'number', description: 'Total number of items returned (channels, members, or users)', }, + nextCursor: { + type: 'string', + description: 'Cursor for the next page (null when there are no more pages)', + }, // slack_list_members outputs (list_members operation) members: { diff --git a/apps/sim/lib/workflows/migrations/subblock-migrations.ts b/apps/sim/lib/workflows/migrations/subblock-migrations.ts index 8161b7d2432..0d49aeefb36 100644 --- a/apps/sim/lib/workflows/migrations/subblock-migrations.ts +++ b/apps/sim/lib/workflows/migrations/subblock-migrations.ts @@ -34,7 +34,8 @@ export const SUBBLOCK_ID_MIGRATIONS: Record> = { ashby: { emailType: '_removed_emailType', phoneType: '_removed_phoneType', - filterCandidateId: '_removed_filterCandidateId', + expandApplicationFormDefinition: '_removed_expandApplicationFormDefinition', + expandSurveyFormDefinitions: '_removed_expandSurveyFormDefinitions', }, rippling: { action: '_removed_action', diff --git a/apps/sim/tools/ashby/add_candidate_tag.ts b/apps/sim/tools/ashby/add_candidate_tag.ts index e013cf63be1..b98294b5695 100644 --- a/apps/sim/tools/ashby/add_candidate_tag.ts +++ b/apps/sim/tools/ashby/add_candidate_tag.ts @@ -1,5 +1,10 @@ import type { AshbyCandidate } from '@/tools/ashby/types' -import { CANDIDATE_OUTPUTS, mapCandidate } from '@/tools/ashby/utils' +import { + ashbyAuthHeaders, + ashbyErrorMessage, + CANDIDATE_OUTPUTS, + mapCandidate, +} from '@/tools/ashby/utils' import type { ToolConfig, ToolResponse } from '@/tools/types' interface AshbyAddCandidateTagParams { @@ -45,10 +50,7 @@ export const addCandidateTagTool: ToolConfig< request: { url: 'https://api.ashbyhq.com/candidate.addTag', method: 'POST', - headers: (params) => ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), + headers: (params) => ashbyAuthHeaders(params.apiKey), body: (params) => ({ candidateId: params.candidateId.trim(), tagId: params.tagId.trim(), @@ -59,7 +61,7 @@ export const addCandidateTagTool: ToolConfig< const data = await response.json() if (!data.success) { - throw new Error(data.errorInfo?.message || 'Failed to add tag to candidate') + throw new Error(ashbyErrorMessage(data, 'Failed to add tag to candidate')) } return { diff --git a/apps/sim/tools/ashby/change_application_stage.ts b/apps/sim/tools/ashby/change_application_stage.ts index c573b04df3e..5140c8cbeab 100644 --- a/apps/sim/tools/ashby/change_application_stage.ts +++ b/apps/sim/tools/ashby/change_application_stage.ts @@ -1,5 +1,10 @@ import type { AshbyApplication } from '@/tools/ashby/types' -import { APPLICATION_OUTPUTS, mapApplication } from '@/tools/ashby/utils' +import { + APPLICATION_OUTPUTS, + ashbyAuthHeaders, + ashbyErrorMessage, + mapApplication, +} from '@/tools/ashby/utils' import type { ToolConfig, ToolResponse } from '@/tools/types' interface AshbyChangeApplicationStageParams { @@ -54,10 +59,7 @@ export const changeApplicationStageTool: ToolConfig< request: { url: 'https://api.ashbyhq.com/application.changeStage', method: 'POST', - headers: (params) => ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), + headers: (params) => ashbyAuthHeaders(params.apiKey), body: (params) => { const body: Record = { applicationId: params.applicationId.trim(), @@ -72,7 +74,7 @@ export const changeApplicationStageTool: ToolConfig< const data = await response.json() if (!data.success) { - throw new Error(data.errorInfo?.message || 'Failed to change application stage') + throw new Error(ashbyErrorMessage(data, 'Failed to change application stage')) } return { diff --git a/apps/sim/tools/ashby/create_application.ts b/apps/sim/tools/ashby/create_application.ts index 5482446de7f..a61f24131da 100644 --- a/apps/sim/tools/ashby/create_application.ts +++ b/apps/sim/tools/ashby/create_application.ts @@ -1,5 +1,10 @@ import type { AshbyApplication } from '@/tools/ashby/types' -import { APPLICATION_OUTPUTS, mapApplication } from '@/tools/ashby/utils' +import { + APPLICATION_OUTPUTS, + ashbyAuthHeaders, + ashbyErrorMessage, + mapApplication, +} from '@/tools/ashby/utils' import type { ToolConfig, ToolResponse } from '@/tools/types' interface AshbyCreateApplicationParams { @@ -82,10 +87,7 @@ export const createApplicationTool: ToolConfig< request: { url: 'https://api.ashbyhq.com/application.create', method: 'POST', - headers: (params) => ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), + headers: (params) => ashbyAuthHeaders(params.apiKey), body: (params) => { const body: Record = { candidateId: params.candidateId.trim(), @@ -104,7 +106,7 @@ export const createApplicationTool: ToolConfig< const data = await response.json() if (!data.success) { - throw new Error(data.errorInfo?.message || 'Failed to create application') + throw new Error(ashbyErrorMessage(data, 'Failed to create application')) } return { diff --git a/apps/sim/tools/ashby/create_candidate.ts b/apps/sim/tools/ashby/create_candidate.ts index 49d58342a3e..813f4ad2f4a 100644 --- a/apps/sim/tools/ashby/create_candidate.ts +++ b/apps/sim/tools/ashby/create_candidate.ts @@ -1,5 +1,10 @@ import type { AshbyCreateCandidateParams, AshbyCreateCandidateResponse } from '@/tools/ashby/types' -import { CANDIDATE_OUTPUTS, mapCandidate } from '@/tools/ashby/utils' +import { + ashbyAuthHeaders, + ashbyErrorMessage, + CANDIDATE_OUTPUTS, + mapCandidate, +} from '@/tools/ashby/utils' import type { ToolConfig } from '@/tools/types' export const createCandidateTool: ToolConfig< @@ -48,21 +53,44 @@ export const createCandidateTool: ToolConfig< visibility: 'user-or-llm', description: 'GitHub profile URL', }, + website: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Personal website URL', + }, sourceId: { type: 'string', required: false, visibility: 'user-or-llm', description: 'UUID of the source to attribute the candidate to', }, + creditedToUserId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'UUID of the Ashby user to credit with sourcing this candidate', + }, + createdAt: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Backdated creation timestamp in ISO 8601 (e.g. 2024-01-01T00:00:00Z). Defaults to now.', + }, + alternateEmailAddresses: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + 'Array of additional email address strings to add to the candidate, e.g. ["a@x.com","b@y.com"]', + }, }, request: { url: 'https://api.ashbyhq.com/candidate.create', method: 'POST', - headers: (params) => ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), + headers: (params) => ashbyAuthHeaders(params.apiKey), body: (params) => { const body: Record = { name: params.name, @@ -71,7 +99,15 @@ export const createCandidateTool: ToolConfig< if (params.phoneNumber) body.phoneNumber = params.phoneNumber if (params.linkedInUrl) body.linkedInUrl = params.linkedInUrl if (params.githubUrl) body.githubUrl = params.githubUrl + if (params.website) body.website = params.website if (params.sourceId) body.sourceId = params.sourceId.trim() + if (params.creditedToUserId) body.creditedToUserId = params.creditedToUserId.trim() + if (params.createdAt) body.createdAt = params.createdAt + if ( + Array.isArray(params.alternateEmailAddresses) && + params.alternateEmailAddresses.length > 0 + ) + body.alternateEmailAddresses = params.alternateEmailAddresses return body }, }, @@ -80,7 +116,7 @@ export const createCandidateTool: ToolConfig< const data = await response.json() if (!data.success) { - throw new Error(data.errorInfo?.message || 'Failed to create candidate') + throw new Error(ashbyErrorMessage(data, 'Failed to create candidate')) } return { diff --git a/apps/sim/tools/ashby/create_note.ts b/apps/sim/tools/ashby/create_note.ts index 03e1ec15546..ed9fab26489 100644 --- a/apps/sim/tools/ashby/create_note.ts +++ b/apps/sim/tools/ashby/create_note.ts @@ -1,4 +1,5 @@ import type { AshbyCreateNoteParams, AshbyCreateNoteResponse } from '@/tools/ashby/types' +import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils' import type { ToolConfig } from '@/tools/types' export const createNoteTool: ToolConfig = { @@ -40,15 +41,25 @@ export const createNoteTool: ToolConfig ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), + headers: (params) => ashbyAuthHeaders(params.apiKey), body: (params) => { const body: Record = { candidateId: params.candidateId.trim(), @@ -62,6 +73,8 @@ export const createNoteTool: ToolConfig ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), + headers: (params) => ashbyAuthHeaders(params.apiKey), body: (params) => ({ applicationId: params.applicationId.trim(), }), @@ -51,7 +53,7 @@ export const getApplicationTool: ToolConfig< const data = await response.json() if (!data.success) { - throw new Error(data.errorInfo?.message || 'Failed to get application') + throw new Error(ashbyErrorMessage(data, 'Failed to get application')) } return { diff --git a/apps/sim/tools/ashby/get_candidate.ts b/apps/sim/tools/ashby/get_candidate.ts index c6aed78aa4e..c3d8d692b3c 100644 --- a/apps/sim/tools/ashby/get_candidate.ts +++ b/apps/sim/tools/ashby/get_candidate.ts @@ -1,5 +1,10 @@ import type { AshbyGetCandidateParams, AshbyGetCandidateResponse } from '@/tools/ashby/types' -import { CANDIDATE_OUTPUTS, mapCandidate } from '@/tools/ashby/utils' +import { + ashbyAuthHeaders, + ashbyErrorMessage, + CANDIDATE_OUTPUTS, + mapCandidate, +} from '@/tools/ashby/utils' import type { ToolConfig } from '@/tools/types' export const getCandidateTool: ToolConfig = { @@ -26,10 +31,7 @@ export const getCandidateTool: ToolConfig ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), + headers: (params) => ashbyAuthHeaders(params.apiKey), body: (params) => ({ id: params.candidateId.trim(), }), @@ -39,7 +41,7 @@ export const getCandidateTool: ToolConfig = { @@ -26,12 +26,10 @@ export const getJobTool: ToolConfig = { request: { url: 'https://api.ashbyhq.com/job.info', method: 'POST', - headers: (params) => ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), + headers: (params) => ashbyAuthHeaders(params.apiKey), body: (params) => ({ id: params.jobId.trim(), + expand: ['openings', 'location', 'compensation'], }), }, @@ -39,7 +37,7 @@ export const getJobTool: ToolConfig = { const data = await response.json() if (!data.success) { - throw new Error(data.errorInfo?.message || 'Failed to get job') + throw new Error(ashbyErrorMessage(data, 'Failed to get job')) } return { diff --git a/apps/sim/tools/ashby/get_job_posting.ts b/apps/sim/tools/ashby/get_job_posting.ts index fc0d973c549..87bb38e0008 100644 --- a/apps/sim/tools/ashby/get_job_posting.ts +++ b/apps/sim/tools/ashby/get_job_posting.ts @@ -1,10 +1,11 @@ +import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils' import type { ToolConfig, ToolResponse } from '@/tools/types' interface AshbyGetJobPostingParams { apiKey: string jobPostingId: string - expandApplicationFormDefinition?: boolean - expandSurveyFormDefinitions?: boolean + jobBoardId?: string + expandJob?: boolean } interface AshbyDescriptionPart { @@ -65,6 +66,7 @@ interface AshbyJobPosting { } | null applicationLimitCalloutHtml: string | null updatedAt: string | null + job: Record | null } interface AshbyGetJobPostingResponse extends ToolResponse { @@ -99,37 +101,31 @@ export const getJobPostingTool: ToolConfig ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), + headers: (params) => ashbyAuthHeaders(params.apiKey), body: (params) => { const body: Record = { jobPostingId: params.jobPostingId.trim(), } - if (params.expandApplicationFormDefinition !== undefined) { - body.expandApplicationFormDefinition = params.expandApplicationFormDefinition - } - if (params.expandSurveyFormDefinitions !== undefined) { - body.expandSurveyFormDefinitions = params.expandSurveyFormDefinitions - } + if (params.jobBoardId) body.jobBoardId = params.jobBoardId.trim() + if (params.expandJob) body.expand = ['job'] return body }, }, @@ -138,7 +134,7 @@ export const getJobPostingTool: ToolConfig & { @@ -225,6 +221,7 @@ export const getJobPostingTool: ToolConfig) ?? null, }, } }, @@ -407,7 +404,7 @@ export const getJobPostingTool: ToolConfig ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), + headers: (params) => ashbyAuthHeaders(params.apiKey), body: (params) => ({ offerId: params.offerId.trim(), }), @@ -48,7 +45,7 @@ export const getOfferTool: ToolConfig ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), + headers: (params) => ashbyAuthHeaders(params.apiKey), body: (params) => { const body: Record = {} if (params.cursor) body.cursor = params.cursor if (params.perPage) body.limit = params.perPage if (params.status) body.status = params.status if (params.jobId) body.jobId = params.jobId.trim() + if (params.candidateId) body.candidateId = params.candidateId.trim() if (params.createdAfter) { const ms = new Date(params.createdAfter).getTime() if (!Number.isNaN(ms)) body.createdAfter = ms @@ -80,7 +89,7 @@ export const listApplicationsTool: ToolConfig< const data = await response.json() if (!data.success) { - throw new Error(data.errorInfo?.message || 'Failed to list applications') + throw new Error(ashbyErrorMessage(data, 'Failed to list applications')) } return { diff --git a/apps/sim/tools/ashby/list_archive_reasons.ts b/apps/sim/tools/ashby/list_archive_reasons.ts index eedf0d491f5..021468d04c8 100644 --- a/apps/sim/tools/ashby/list_archive_reasons.ts +++ b/apps/sim/tools/ashby/list_archive_reasons.ts @@ -1,3 +1,4 @@ +import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils' import type { ToolConfig, ToolResponse } from '@/tools/types' interface AshbyListArchiveReasonsParams { @@ -45,10 +46,7 @@ export const listArchiveReasonsTool: ToolConfig< request: { url: 'https://api.ashbyhq.com/archiveReason.list', method: 'POST', - headers: (params) => ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), + headers: (params) => ashbyAuthHeaders(params.apiKey), body: (params) => { const body: Record = {} if (params.includeArchived !== undefined) body.includeArchived = params.includeArchived @@ -60,7 +58,7 @@ export const listArchiveReasonsTool: ToolConfig< const data = await response.json() if (!data.success) { - throw new Error(data.errorInfo?.message || 'Failed to list archive reasons') + throw new Error(ashbyErrorMessage(data, 'Failed to list archive reasons')) } return { diff --git a/apps/sim/tools/ashby/list_candidate_tags.ts b/apps/sim/tools/ashby/list_candidate_tags.ts index 68025ea29be..7c1886f8cd7 100644 --- a/apps/sim/tools/ashby/list_candidate_tags.ts +++ b/apps/sim/tools/ashby/list_candidate_tags.ts @@ -1,3 +1,4 @@ +import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils' import type { ToolConfig, ToolResponse } from '@/tools/types' interface AshbyListCandidateTagsParams { @@ -68,10 +69,7 @@ export const listCandidateTagsTool: ToolConfig< request: { url: 'https://api.ashbyhq.com/candidateTag.list', method: 'POST', - headers: (params) => ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), + headers: (params) => ashbyAuthHeaders(params.apiKey), body: (params) => { const body: Record = {} if (params.includeArchived !== undefined) body.includeArchived = params.includeArchived @@ -86,7 +84,7 @@ export const listCandidateTagsTool: ToolConfig< const data = await response.json() if (!data.success) { - throw new Error(data.errorInfo?.message || 'Failed to list candidate tags') + throw new Error(ashbyErrorMessage(data, 'Failed to list candidate tags')) } return { diff --git a/apps/sim/tools/ashby/list_candidates.ts b/apps/sim/tools/ashby/list_candidates.ts index 7af25ccb83b..00ef718af10 100644 --- a/apps/sim/tools/ashby/list_candidates.ts +++ b/apps/sim/tools/ashby/list_candidates.ts @@ -1,5 +1,10 @@ import type { AshbyListCandidatesParams, AshbyListCandidatesResponse } from '@/tools/ashby/types' -import { CANDIDATE_OUTPUTS, mapCandidate } from '@/tools/ashby/utils' +import { + ashbyAuthHeaders, + ashbyErrorMessage, + CANDIDATE_OUTPUTS, + mapCandidate, +} from '@/tools/ashby/utils' import type { ToolConfig } from '@/tools/types' export const listCandidatesTool: ToolConfig< @@ -30,19 +35,27 @@ export const listCandidatesTool: ToolConfig< visibility: 'user-or-llm', description: 'Number of results per page (default 100)', }, + createdAfter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Only return candidates created after this ISO 8601 timestamp (e.g. 2024-01-01T00:00:00Z)', + }, }, request: { url: 'https://api.ashbyhq.com/candidate.list', method: 'POST', - headers: (params) => ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), + headers: (params) => ashbyAuthHeaders(params.apiKey), body: (params) => { const body: Record = {} if (params.cursor) body.cursor = params.cursor if (params.perPage) body.limit = params.perPage + if (params.createdAfter) { + const ms = new Date(params.createdAfter).getTime() + if (!Number.isNaN(ms)) body.createdAfter = ms + } return body }, }, @@ -51,7 +64,7 @@ export const listCandidatesTool: ToolConfig< const data = await response.json() if (!data.success) { - throw new Error(data.errorInfo?.message || 'Failed to list candidates') + throw new Error(ashbyErrorMessage(data, 'Failed to list candidates')) } return { diff --git a/apps/sim/tools/ashby/list_custom_fields.ts b/apps/sim/tools/ashby/list_custom_fields.ts index ca35233391f..2c2d8fb97da 100644 --- a/apps/sim/tools/ashby/list_custom_fields.ts +++ b/apps/sim/tools/ashby/list_custom_fields.ts @@ -1,7 +1,12 @@ +import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils' import type { ToolConfig, ToolResponse } from '@/tools/types' interface AshbyListCustomFieldsParams { apiKey: string + cursor?: string + perPage?: number + syncToken?: string + includeArchived?: boolean } interface AshbyCustomFieldDefinition { @@ -22,6 +27,9 @@ interface AshbyCustomFieldDefinition { interface AshbyListCustomFieldsResponse extends ToolResponse { output: { customFields: AshbyCustomFieldDefinition[] + moreDataAvailable: boolean + nextCursor: string | null + syncToken: string | null } } @@ -41,28 +49,59 @@ export const listCustomFieldsTool: ToolConfig< visibility: 'user-only', description: 'Ashby API Key', }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Opaque pagination cursor from a previous response nextCursor value', + }, + perPage: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (default and max 100)', + }, + syncToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Opaque token from a prior sync to fetch only items changed since then', + }, + includeArchived: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'When true, includes archived custom fields in results (default false)', + }, }, request: { url: 'https://api.ashbyhq.com/customField.list', method: 'POST', - headers: (params) => ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), - body: () => ({}), + headers: (params) => ashbyAuthHeaders(params.apiKey), + body: (params) => { + const body: Record = {} + if (params.cursor) body.cursor = params.cursor + if (params.perPage) body.limit = params.perPage + if (params.syncToken) body.syncToken = params.syncToken + if (params.includeArchived !== undefined) body.includeArchived = params.includeArchived + return body + }, }, transformResponse: async (response: Response) => { const data = await response.json() if (!data.success) { - throw new Error(data.errorInfo?.message || 'Failed to list custom fields') + throw new Error(ashbyErrorMessage(data, 'Failed to list custom fields')) } return { success: true, output: { + moreDataAvailable: data.moreDataAvailable ?? false, + nextCursor: data.nextCursor ?? null, + syncToken: data.syncToken ?? null, customFields: (data.results ?? []).map( (f: Record & { selectableValues?: Array> }) => ({ id: (f.id as string) ?? '', @@ -126,5 +165,19 @@ export const listCustomFieldsTool: ToolConfig< }, }, }, + moreDataAvailable: { + type: 'boolean', + description: 'Whether more pages of results exist', + }, + nextCursor: { + type: 'string', + description: 'Opaque cursor for fetching the next page', + optional: true, + }, + syncToken: { + type: 'string', + description: 'Opaque sync token returned after the last page; pass on next sync', + optional: true, + }, }, } diff --git a/apps/sim/tools/ashby/list_departments.ts b/apps/sim/tools/ashby/list_departments.ts index e1e2acabece..4f671856bf8 100644 --- a/apps/sim/tools/ashby/list_departments.ts +++ b/apps/sim/tools/ashby/list_departments.ts @@ -1,7 +1,12 @@ +import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils' import type { ToolConfig, ToolResponse } from '@/tools/types' interface AshbyListDepartmentsParams { apiKey: string + cursor?: string + perPage?: number + syncToken?: string + includeArchived?: boolean } interface AshbyDepartment { @@ -12,11 +17,15 @@ interface AshbyDepartment { parentId: string | null createdAt: string | null updatedAt: string | null + extraData: Record | null } interface AshbyListDepartmentsResponse extends ToolResponse { output: { departments: AshbyDepartment[] + moreDataAvailable: boolean + nextCursor: string | null + syncToken: string | null } } @@ -36,23 +45,51 @@ export const listDepartmentsTool: ToolConfig< visibility: 'user-only', description: 'Ashby API Key', }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Opaque pagination cursor from a previous response nextCursor value', + }, + perPage: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (default and max 100)', + }, + syncToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Opaque token from a prior sync to fetch only items changed since then', + }, + includeArchived: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'When true, includes archived departments in results (default false)', + }, }, request: { url: 'https://api.ashbyhq.com/department.list', method: 'POST', - headers: (params) => ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), - body: () => ({}), + headers: (params) => ashbyAuthHeaders(params.apiKey), + body: (params) => { + const body: Record = {} + if (params.cursor) body.cursor = params.cursor + if (params.perPage) body.limit = params.perPage + if (params.syncToken) body.syncToken = params.syncToken + if (params.includeArchived !== undefined) body.includeArchived = params.includeArchived + return body + }, }, transformResponse: async (response: Response) => { const data = await response.json() if (!data.success) { - throw new Error(data.errorInfo?.message || 'Failed to list departments') + throw new Error(ashbyErrorMessage(data, 'Failed to list departments')) } return { @@ -66,7 +103,11 @@ export const listDepartmentsTool: ToolConfig< parentId: (d.parentId as string) ?? null, createdAt: (d.createdAt as string) ?? null, updatedAt: (d.updatedAt as string) ?? null, + extraData: (d.extraData as Record) ?? null, })), + moreDataAvailable: data.moreDataAvailable ?? false, + nextCursor: data.nextCursor ?? null, + syncToken: data.syncToken ?? null, }, } }, @@ -101,8 +142,27 @@ export const listDepartmentsTool: ToolConfig< description: 'ISO 8601 last update timestamp', optional: true, }, + extraData: { + type: 'json', + description: 'Free-form key-value metadata', + optional: true, + }, }, }, }, + moreDataAvailable: { + type: 'boolean', + description: 'Whether more pages of results exist', + }, + nextCursor: { + type: 'string', + description: 'Opaque cursor for fetching the next page', + optional: true, + }, + syncToken: { + type: 'string', + description: 'Opaque sync token returned after the last page; pass on next sync', + optional: true, + }, }, } diff --git a/apps/sim/tools/ashby/list_interviews.ts b/apps/sim/tools/ashby/list_interviews.ts index ae9ae2e95b0..b9daab01b27 100644 --- a/apps/sim/tools/ashby/list_interviews.ts +++ b/apps/sim/tools/ashby/list_interviews.ts @@ -1,5 +1,10 @@ import type { AshbyUserSummary } from '@/tools/ashby/types' -import { mapUserSummary, USER_SUMMARY_OUTPUT } from '@/tools/ashby/utils' +import { + ashbyAuthHeaders, + ashbyErrorMessage, + mapUserSummary, + USER_SUMMARY_OUTPUT, +} from '@/tools/ashby/utils' import type { ToolConfig, ToolResponse } from '@/tools/types' interface AshbyListInterviewSchedulesParams { @@ -8,6 +13,7 @@ interface AshbyListInterviewSchedulesParams { interviewStageId?: string cursor?: string perPage?: number + createdAfter?: string } interface AshbyInterviewEvent { @@ -126,21 +132,29 @@ export const listInterviewsTool: ToolConfig< visibility: 'user-or-llm', description: 'Number of results per page (default 100)', }, + createdAfter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Only return interview schedules created after this ISO 8601 timestamp (e.g. 2024-01-01T00:00:00Z)', + }, }, request: { url: 'https://api.ashbyhq.com/interviewSchedule.list', method: 'POST', - headers: (params) => ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), + headers: (params) => ashbyAuthHeaders(params.apiKey), body: (params) => { const body: Record = {} if (params.applicationId) body.applicationId = params.applicationId.trim() if (params.interviewStageId) body.interviewStageId = params.interviewStageId.trim() if (params.cursor) body.cursor = params.cursor if (params.perPage) body.limit = params.perPage + if (params.createdAfter) { + const ms = new Date(params.createdAfter).getTime() + if (!Number.isNaN(ms)) body.createdAfter = ms + } return body }, }, @@ -149,7 +163,7 @@ export const listInterviewsTool: ToolConfig< const data = await response.json() if (!data.success) { - throw new Error(data.errorInfo?.message || 'Failed to list interview schedules') + throw new Error(ashbyErrorMessage(data, 'Failed to list interview schedules')) } return { diff --git a/apps/sim/tools/ashby/list_job_postings.ts b/apps/sim/tools/ashby/list_job_postings.ts index b702695ed56..aded90c1cac 100644 --- a/apps/sim/tools/ashby/list_job_postings.ts +++ b/apps/sim/tools/ashby/list_job_postings.ts @@ -1,7 +1,12 @@ +import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils' import type { ToolConfig, ToolResponse } from '@/tools/types' interface AshbyListJobPostingsParams { apiKey: string + location?: string + department?: string + listedOnly?: boolean + jobBoardId?: string } interface AshbyJobPostingSummary { @@ -49,23 +54,52 @@ export const listJobPostingsTool: ToolConfig< visibility: 'user-only', description: 'Ashby API Key', }, + location: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by location name (case sensitive)', + }, + department: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by department name (case sensitive)', + }, + listedOnly: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'When true, only returns listed (publicly visible) job postings (default false)', + }, + jobBoardId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'UUID of a specific job board to filter postings to. If omitted, returns postings on the primary external job board.', + }, }, request: { url: 'https://api.ashbyhq.com/jobPosting.list', method: 'POST', - headers: (params) => ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), - body: () => ({}), + headers: (params) => ashbyAuthHeaders(params.apiKey), + body: (params) => { + const body: Record = {} + if (params.location) body.location = params.location + if (params.department) body.department = params.department + if (params.listedOnly !== undefined) body.listedOnly = params.listedOnly + if (params.jobBoardId) body.jobBoardId = params.jobBoardId.trim() + return body + }, }, transformResponse: async (response: Response) => { const data = await response.json() if (!data.success) { - throw new Error(data.errorInfo?.message || 'Failed to list job postings') + throw new Error(ashbyErrorMessage(data, 'Failed to list job postings')) } return { diff --git a/apps/sim/tools/ashby/list_jobs.ts b/apps/sim/tools/ashby/list_jobs.ts index 691ec2cf342..13457a1732e 100644 --- a/apps/sim/tools/ashby/list_jobs.ts +++ b/apps/sim/tools/ashby/list_jobs.ts @@ -1,5 +1,5 @@ import type { AshbyListJobsParams, AshbyListJobsResponse } from '@/tools/ashby/types' -import { JOB_OUTPUTS, mapJob } from '@/tools/ashby/utils' +import { ashbyAuthHeaders, ashbyErrorMessage, JOB_OUTPUTS, mapJob } from '@/tools/ashby/utils' import type { ToolConfig } from '@/tools/types' export const listJobsTool: ToolConfig = { @@ -34,20 +34,72 @@ export const listJobsTool: ToolConfig ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), + headers: (params) => ashbyAuthHeaders(params.apiKey), body: (params) => { - const body: Record = {} + const body: Record = { expand: ['openings', 'location'] } if (params.cursor) body.cursor = params.cursor if (params.perPage) body.limit = params.perPage if (params.status) body.status = [params.status] + const isoToMs = (iso: string): number | null => { + const ms = new Date(iso).getTime() + return Number.isNaN(ms) ? null : ms + } + if (params.createdAfter) { + const ms = isoToMs(params.createdAfter) + if (ms !== null) body.createdAfter = ms + } + if (params.openedAfter) { + const ms = isoToMs(params.openedAfter) + if (ms !== null) body.openedAfter = ms + } + if (params.openedBefore) { + const ms = isoToMs(params.openedBefore) + if (ms !== null) body.openedBefore = ms + } + if (params.closedAfter) { + const ms = isoToMs(params.closedAfter) + if (ms !== null) body.closedAfter = ms + } + if (params.closedBefore) { + const ms = isoToMs(params.closedBefore) + if (ms !== null) body.closedBefore = ms + } return body }, }, @@ -56,7 +108,7 @@ export const listJobsTool: ToolConfig | null } interface AshbyListLocationsResponse extends ToolResponse { output: { locations: AshbyLocation[] + moreDataAvailable: boolean + nextCursor: string | null + syncToken: string | null } } @@ -41,23 +51,59 @@ export const listLocationsTool: ToolConfig ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), - body: () => ({}), + headers: (params) => ashbyAuthHeaders(params.apiKey), + body: (params) => { + const body: Record = {} + if (params.cursor) body.cursor = params.cursor + if (params.perPage) body.limit = params.perPage + if (params.syncToken) body.syncToken = params.syncToken + if (params.includeArchived !== undefined) body.includeArchived = params.includeArchived + if (params.includeLocationHierarchy !== undefined) + body.includeLocationHierarchy = params.includeLocationHierarchy + return body + }, }, transformResponse: async (response: Response) => { const data = await response.json() if (!data.success) { - throw new Error(data.errorInfo?.message || 'Failed to list locations') + throw new Error(ashbyErrorMessage(data, 'Failed to list locations')) } return { @@ -88,9 +134,13 @@ export const listLocationsTool: ToolConfig) ?? null, } } ), + moreDataAvailable: data.moreDataAvailable ?? false, + nextCursor: data.nextCursor ?? null, + syncToken: data.syncToken ?? null, }, } }, @@ -149,8 +199,27 @@ export const listLocationsTool: ToolConfig ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), + headers: (params) => ashbyAuthHeaders(params.apiKey), body: (params) => { const body: Record = { candidateId: params.candidateId.trim(), @@ -80,7 +78,7 @@ export const listNotesTool: ToolConfig ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), + headers: (params) => ashbyAuthHeaders(params.apiKey), body: (params) => { const body: Record = {} if (params.cursor) body.cursor = params.cursor if (params.perPage) body.limit = params.perPage + if (params.createdAfter) { + const ms = new Date(params.createdAfter).getTime() + if (!Number.isNaN(ms)) body.createdAfter = ms + } + if (params.syncToken) body.syncToken = params.syncToken + if (params.applicationId) body.applicationId = params.applicationId.trim() return body }, }, @@ -62,7 +87,7 @@ export const listOffersTool: ToolConfig ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), + headers: (params) => ashbyAuthHeaders(params.apiKey), body: (params) => { const body: Record = {} if (params.cursor) body.cursor = params.cursor if (params.perPage) body.limit = params.perPage + if (params.createdAfter) { + const ms = new Date(params.createdAfter).getTime() + if (!Number.isNaN(ms)) body.createdAfter = ms + } return body }, }, @@ -62,7 +76,7 @@ export const listOpeningsTool: ToolConfig ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), - body: () => ({}), + headers: (params) => ashbyAuthHeaders(params.apiKey), + body: (params) => { + const body: Record = {} + if (params.includeArchived !== undefined) body.includeArchived = params.includeArchived + return body + }, }, transformResponse: async (response: Response) => { const data = await response.json() if (!data.success) { - throw new Error(data.errorInfo?.message || 'Failed to list sources') + throw new Error(ashbyErrorMessage(data, 'Failed to list sources')) } return { diff --git a/apps/sim/tools/ashby/list_users.ts b/apps/sim/tools/ashby/list_users.ts index 486e1eaadd4..7f9250d2f43 100644 --- a/apps/sim/tools/ashby/list_users.ts +++ b/apps/sim/tools/ashby/list_users.ts @@ -1,11 +1,17 @@ import type { AshbyUserSummary } from '@/tools/ashby/types' -import { mapUserSummary, USER_SUMMARY_OUTPUT } from '@/tools/ashby/utils' +import { + ashbyAuthHeaders, + ashbyErrorMessage, + mapUserSummary, + USER_SUMMARY_OUTPUT, +} from '@/tools/ashby/utils' import type { ToolConfig, ToolResponse } from '@/tools/types' interface AshbyListUsersParams { apiKey: string cursor?: string perPage?: number + includeDeactivated?: boolean } interface AshbyListUsersResponse extends ToolResponse { @@ -41,19 +47,24 @@ export const listUsersTool: ToolConfig ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), + headers: (params) => ashbyAuthHeaders(params.apiKey), body: (params) => { const body: Record = {} if (params.cursor) body.cursor = params.cursor if (params.perPage) body.limit = params.perPage + if (params.includeDeactivated !== undefined) + body.includeDeactivated = params.includeDeactivated return body }, }, @@ -62,7 +73,7 @@ export const listUsersTool: ToolConfig ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), + headers: (params) => ashbyAuthHeaders(params.apiKey), body: (params) => ({ candidateId: params.candidateId.trim(), tagId: params.tagId.trim(), @@ -59,7 +61,7 @@ export const removeCandidateTagTool: ToolConfig< const data = await response.json() if (!data.success) { - throw new Error(data.errorInfo?.message || 'Failed to remove tag from candidate') + throw new Error(ashbyErrorMessage(data, 'Failed to remove tag from candidate')) } return { diff --git a/apps/sim/tools/ashby/search_candidates.ts b/apps/sim/tools/ashby/search_candidates.ts index 41a0ecf4dd4..713f8dbe8dd 100644 --- a/apps/sim/tools/ashby/search_candidates.ts +++ b/apps/sim/tools/ashby/search_candidates.ts @@ -2,7 +2,12 @@ import type { AshbySearchCandidatesParams, AshbySearchCandidatesResponse, } from '@/tools/ashby/types' -import { CANDIDATE_OUTPUTS, mapCandidate } from '@/tools/ashby/utils' +import { + ashbyAuthHeaders, + ashbyErrorMessage, + CANDIDATE_OUTPUTS, + mapCandidate, +} from '@/tools/ashby/utils' import type { ToolConfig } from '@/tools/types' export const searchCandidatesTool: ToolConfig< @@ -39,10 +44,7 @@ export const searchCandidatesTool: ToolConfig< request: { url: 'https://api.ashbyhq.com/candidate.search', method: 'POST', - headers: (params) => ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), + headers: (params) => ashbyAuthHeaders(params.apiKey), body: (params) => { const body: Record = {} if (params.name) body.name = params.name @@ -55,7 +57,7 @@ export const searchCandidatesTool: ToolConfig< const data = await response.json() if (!data.success) { - throw new Error(data.errorInfo?.message || 'Failed to search candidates') + throw new Error(ashbyErrorMessage(data, 'Failed to search candidates')) } return { diff --git a/apps/sim/tools/ashby/types.ts b/apps/sim/tools/ashby/types.ts index daac99dd777..328cb5e1471 100644 --- a/apps/sim/tools/ashby/types.ts +++ b/apps/sim/tools/ashby/types.ts @@ -94,6 +94,7 @@ export interface AshbyCandidate { export interface AshbyListCandidatesParams extends AshbyBaseParams { cursor?: string perPage?: number + createdAfter?: string } export interface AshbyGetCandidateParams extends AshbyBaseParams { @@ -106,7 +107,11 @@ export interface AshbyCreateCandidateParams extends AshbyBaseParams { phoneNumber?: string linkedInUrl?: string githubUrl?: string + website?: string sourceId?: string + creditedToUserId?: string + createdAt?: string + alternateEmailAddresses?: string[] } export interface AshbySearchCandidatesParams extends AshbyBaseParams { @@ -118,6 +123,11 @@ export interface AshbyListJobsParams extends AshbyBaseParams { cursor?: string perPage?: number status?: string + createdAfter?: string + openedAfter?: string + openedBefore?: string + closedAfter?: string + closedBefore?: string } export interface AshbyGetJobParams extends AshbyBaseParams { @@ -129,6 +139,8 @@ export interface AshbyCreateNoteParams extends AshbyBaseParams { note: string noteType?: string sendNotifications?: boolean + isPrivate?: boolean + createdAt?: string } export interface AshbyListApplicationsParams extends AshbyBaseParams { @@ -136,6 +148,7 @@ export interface AshbyListApplicationsParams extends AshbyBaseParams { perPage?: number status?: string jobId?: string + candidateId?: string createdAfter?: string } @@ -309,6 +322,15 @@ export interface AshbyApplicationArchiveReason { customFields: AshbyCustomField[] } +export interface AshbyApplicationHistoryEntry { + id: string + stageId: string | null + stageNumber: number | null + title: string | null + enteredStageAt: string | null + actorId: string | null +} + export interface AshbyApplication { id: string createdAt: string | null @@ -326,6 +348,7 @@ export interface AshbyApplication { appliedViaJobPostingId: string | null submitterClientIp: string | null submitterUserAgent: string | null + applicationHistory: AshbyApplicationHistoryEntry[] } export interface AshbyListApplicationsResponse extends ToolResponse { diff --git a/apps/sim/tools/ashby/update_candidate.ts b/apps/sim/tools/ashby/update_candidate.ts index abb461f196f..c7fbbf825d4 100644 --- a/apps/sim/tools/ashby/update_candidate.ts +++ b/apps/sim/tools/ashby/update_candidate.ts @@ -1,5 +1,10 @@ import type { AshbyGetCandidateResponse } from '@/tools/ashby/types' -import { CANDIDATE_OUTPUTS, mapCandidate } from '@/tools/ashby/utils' +import { + ashbyAuthHeaders, + ashbyErrorMessage, + CANDIDATE_OUTPUTS, + mapCandidate, +} from '@/tools/ashby/utils' import type { ToolConfig } from '@/tools/types' interface AshbyUpdateCandidateParams { @@ -11,7 +16,12 @@ interface AshbyUpdateCandidateParams { linkedInUrl?: string githubUrl?: string websiteUrl?: string + alternateEmail?: string sourceId?: string + creditedToUserId?: string + createdAt?: string + sendNotifications?: boolean + socialLinks?: Array<{ type: string; url: string }> } export const updateCandidateTool: ToolConfig< @@ -72,21 +82,50 @@ export const updateCandidateTool: ToolConfig< visibility: 'user-or-llm', description: 'Personal website URL', }, + alternateEmail: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'An additional email address to add to the candidate', + }, sourceId: { type: 'string', required: false, visibility: 'user-or-llm', description: 'UUID of the source to attribute the candidate to', }, + creditedToUserId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'UUID of the Ashby user to credit with sourcing this candidate', + }, + createdAt: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Backdated creation timestamp in ISO 8601. Only updatable if originally backdated.', + }, + sendNotifications: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to send a notification when the source is updated (default true)', + }, + socialLinks: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + 'Array of social link objects to set on the candidate, e.g. [{"type":"LinkedIn","url":"https://..."}]. Replaces existing social links.', + }, }, request: { url: 'https://api.ashbyhq.com/candidate.update', method: 'POST', - headers: (params) => ({ - 'Content-Type': 'application/json', - Authorization: `Basic ${btoa(`${params.apiKey}:`)}`, - }), + headers: (params) => ashbyAuthHeaders(params.apiKey), body: (params) => { const body: Record = { candidateId: params.candidateId.trim(), @@ -97,7 +136,13 @@ export const updateCandidateTool: ToolConfig< if (params.linkedInUrl) body.linkedInUrl = params.linkedInUrl if (params.githubUrl) body.githubUrl = params.githubUrl if (params.websiteUrl) body.websiteUrl = params.websiteUrl + if (params.alternateEmail) body.alternateEmail = params.alternateEmail if (params.sourceId) body.sourceId = params.sourceId.trim() + if (params.creditedToUserId) body.creditedToUserId = params.creditedToUserId.trim() + if (params.createdAt) body.createdAt = params.createdAt + if (params.sendNotifications !== undefined) body.sendNotifications = params.sendNotifications + if (Array.isArray(params.socialLinks) && params.socialLinks.length > 0) + body.socialLinks = params.socialLinks return body }, }, @@ -106,7 +151,7 @@ export const updateCandidateTool: ToolConfig< const data = await response.json() if (!data.success) { - throw new Error(data.errorInfo?.message || 'Failed to update candidate') + throw new Error(ashbyErrorMessage(data, 'Failed to update candidate')) } return { diff --git a/apps/sim/tools/ashby/utils.ts b/apps/sim/tools/ashby/utils.ts index cf7fcda54ef..0a992e4ccff 100644 --- a/apps/sim/tools/ashby/utils.ts +++ b/apps/sim/tools/ashby/utils.ts @@ -17,6 +17,33 @@ import type { OutputProperty } from '@/tools/types' type Unknown = Record +/** + * Build the standard Ashby Authorization header. Ashby uses HTTP Basic auth + * with the API key as the username and an empty password. + */ +export function ashbyAuthHeaders(apiKey: string): Record { + return { + 'Content-Type': 'application/json', + Accept: 'application/json; version=1', + Authorization: `Basic ${btoa(`${apiKey}:`)}`, + } +} + +/** + * Extract a human-readable error message from an Ashby error response. Ashby + * returns errors as either `errorInfo.message` or an `errors` string array. + */ +export function ashbyErrorMessage(data: unknown, fallback: string): string { + if (!data || typeof data !== 'object') return fallback + const d = data as Unknown + const info = d.errorInfo as Unknown | undefined + if (info && typeof info.message === 'string' && info.message) return info.message + if (Array.isArray(d.errors) && d.errors.length > 0) { + return d.errors.map((e) => String(e)).join('; ') + } + return fallback +} + function mapContact(raw: unknown): AshbyContactInfo | null { if (!raw || typeof raw !== 'object') return null const c = raw as Unknown @@ -322,6 +349,16 @@ export function mapApplication(raw: unknown): AshbyApplication { appliedViaJobPostingId: (a.appliedViaJobPostingId as string) ?? null, submitterClientIp: (a.submitterClientIp as string) ?? null, submitterUserAgent: (a.submitterUserAgent as string) ?? null, + applicationHistory: Array.isArray(a.applicationHistory) + ? (a.applicationHistory as Unknown[]).map((h) => ({ + id: (h.id as string) ?? '', + stageId: (h.stageId as string) ?? null, + stageNumber: (h.stageNumber as number) ?? null, + title: (h.title as string) ?? null, + enteredStageAt: (h.enteredStageAt as string) ?? null, + actorId: (h.actorId as string) ?? null, + })) + : [], } } @@ -387,7 +424,7 @@ export const USER_SUMMARY_OUTPUT = { globalRole: { type: 'string', description: 'Role', optional: true }, isEnabled: { type: 'boolean', description: 'Whether enabled' }, updatedAt: { type: 'string', description: 'Last update timestamp', optional: true }, - managerId: { type: 'string', description: 'Manager user UUID', optional: true }, + managerId: { type: 'string', description: "User ID of the user's manager", optional: true }, }, } as const satisfies OutputProperty @@ -591,6 +628,29 @@ export const APPLICATION_OUTPUTS = { }, createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' }, updatedAt: { type: 'string', description: 'ISO 8601 last update timestamp' }, + applicationHistory: { + type: 'array', + description: 'Stage history (only populated by application.info, empty for list endpoints)', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'History entry UUID' }, + stageId: { type: 'string', description: 'Interview stage UUID', optional: true }, + stageNumber: { type: 'number', description: 'Stage order number', optional: true }, + title: { type: 'string', description: 'Stage title at the time', optional: true }, + enteredStageAt: { + type: 'string', + description: 'ISO 8601 timestamp the stage was entered', + optional: true, + }, + actorId: { + type: 'string', + description: 'User UUID who triggered the stage change', + optional: true, + }, + }, + }, + }, } as const satisfies Record export const OPENINGS_OUTPUT = { @@ -823,23 +883,28 @@ export const JOB_OUTPUTS = { openings: OPENINGS_OUTPUT, compensation: { type: 'object', - description: 'Job compensation structure', + description: + 'Compensation tiers for the job. Only present when the request includes the `compensation` expand parameter.', optional: true, properties: { compensationTiers: { type: 'array', - description: 'Compensation tiers', + description: 'List of compensation tiers', items: { type: 'object', properties: { - id: { type: 'string', description: 'Tier UUID', optional: true }, + id: { type: 'string', description: 'Tier ID', optional: true }, title: { type: 'string', description: 'Tier title', optional: true }, additionalInformation: { type: 'string', - description: 'Additional info', + description: 'Additional information about the tier', + optional: true, + }, + tierSummary: { + type: 'string', + description: 'Human-readable summary of the tier', optional: true, }, - tierSummary: { type: 'string', description: 'Tier summary', optional: true }, }, }, }, diff --git a/apps/sim/tools/confluence/delete_space.ts b/apps/sim/tools/confluence/delete_space.ts index 82442c66ad5..e6a2fc9d172 100644 --- a/apps/sim/tools/confluence/delete_space.ts +++ b/apps/sim/tools/confluence/delete_space.ts @@ -14,6 +14,8 @@ export interface ConfluenceDeleteSpaceResponse { ts: string spaceId: string deleted: boolean + longTaskId?: string + longTaskStatusLink?: string } } @@ -83,6 +85,8 @@ export const confluenceDeleteSpaceTool: ToolConfig< ts: new Date().toISOString(), spaceId: data.spaceId ?? '', deleted: true, + longTaskId: data.longTaskId, + longTaskStatusLink: data.longTaskStatusLink, }, } }, @@ -91,5 +95,14 @@ export const confluenceDeleteSpaceTool: ToolConfig< ts: TIMESTAMP_OUTPUT, spaceId: { type: 'string', description: 'Deleted space ID' }, deleted: { type: 'boolean', description: 'Deletion status' }, + longTaskId: { + type: 'string', + description: + 'ID of the long-running deletion task; poll Confluence long-task API to track completion', + }, + longTaskStatusLink: { + type: 'string', + description: 'Relative link to the long-task status endpoint', + }, }, } diff --git a/apps/sim/tools/confluence/types.ts b/apps/sim/tools/confluence/types.ts index 057fc0d350e..50fce5c547d 100644 --- a/apps/sim/tools/confluence/types.ts +++ b/apps/sim/tools/confluence/types.ts @@ -525,7 +525,6 @@ export interface ConfluenceUpdateParams { pageId: string title?: string content?: string - version?: number cloudId?: string } diff --git a/apps/sim/tools/confluence/update.ts b/apps/sim/tools/confluence/update.ts index f9dcd01076e..e5daa6628fd 100644 --- a/apps/sim/tools/confluence/update.ts +++ b/apps/sim/tools/confluence/update.ts @@ -44,12 +44,6 @@ export const confluenceUpdateTool: ToolConfig { - const data = await response.json() + const data = await response.json().catch(() => ({}) as any) if (!response.ok) { - throw new Error(data.error?.message || 'Failed to copy Google Drive file') + throw new Error( + data.error?.message || + `Failed to copy Google Drive file (${response.status} ${response.statusText})` + ) } return { diff --git a/apps/sim/tools/google_drive/create_folder.ts b/apps/sim/tools/google_drive/create_folder.ts index eb80cd14b03..77ce955c322 100644 --- a/apps/sim/tools/google_drive/create_folder.ts +++ b/apps/sim/tools/google_drive/create_folder.ts @@ -61,9 +61,9 @@ export const createFolderTool: ToolConfig { if (!response.ok) { - const data = await response.json() - throw new Error(data.error?.message || 'Failed to delete Google Drive file') + const data = await response.json().catch(() => ({}) as any) + throw new Error( + data.error?.message || + `Failed to delete Google Drive file (${response.status} ${response.statusText})` + ) } return { diff --git a/apps/sim/tools/google_drive/get_content.ts b/apps/sim/tools/google_drive/get_content.ts index e507098d2ef..02237341eeb 100644 --- a/apps/sim/tools/google_drive/get_content.ts +++ b/apps/sim/tools/google_drive/get_content.ts @@ -57,7 +57,7 @@ export const getContentTool: ToolConfig - `https://www.googleapis.com/drive/v3/files/${params.fileId}?fields=${ALL_FILE_FIELDS}&supportsAllDrives=true`, + `https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}?fields=${ALL_FILE_FIELDS}&supportsAllDrives=true`, method: 'GET', headers: (params) => ({ Authorization: `Bearer ${params.accessToken}`, diff --git a/apps/sim/tools/google_drive/get_file.ts b/apps/sim/tools/google_drive/get_file.ts index a4a51d42de7..f27ee4725db 100644 --- a/apps/sim/tools/google_drive/get_file.ts +++ b/apps/sim/tools/google_drive/get_file.ts @@ -52,10 +52,13 @@ export const getFileTool: ToolConfig { - const data = await response.json() + const data = await response.json().catch(() => ({}) as any) if (!response.ok) { - throw new Error(data.error?.message || 'Failed to get Google Drive file') + throw new Error( + data.error?.message || + `Failed to get Google Drive file (${response.status} ${response.statusText})` + ) } return { diff --git a/apps/sim/tools/google_drive/list.ts b/apps/sim/tools/google_drive/list.ts index 32369915f25..afc4ee2c7cb 100644 --- a/apps/sim/tools/google_drive/list.ts +++ b/apps/sim/tools/google_drive/list.ts @@ -68,7 +68,7 @@ export const listTool: ToolConfig { - const data = await response.json() + const data = await response.json().catch(() => ({}) as any) if (!response.ok) { - throw new Error(data.error?.message || 'Failed to update Google Drive file') + throw new Error( + data.error?.message || + `Failed to update Google Drive file (${response.status} ${response.statusText})` + ) } return { diff --git a/apps/sim/tools/jira/add_comment.ts b/apps/sim/tools/jira/add_comment.ts index 29d11f49684..7d784b39029 100644 --- a/apps/sim/tools/jira/add_comment.ts +++ b/apps/sim/tools/jira/add_comment.ts @@ -1,6 +1,6 @@ import type { JiraAddCommentParams, JiraAddCommentResponse } from '@/tools/jira/types' import { SUCCESS_OUTPUT, TIMESTAMP_OUTPUT, USER_OUTPUT_PROPERTIES } from '@/tools/jira/types' -import { extractAdfText, getJiraCloudId, transformUser } from '@/tools/jira/utils' +import { extractAdfText, getJiraCloudId, toAdf, transformUser } from '@/tools/jira/utils' import type { ToolConfig } from '@/tools/types' /** @@ -74,7 +74,7 @@ export const jiraAddCommentTool: ToolConfig { if (params.cloudId) { - return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/comment` + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}/comment` } return 'https://api.atlassian.com/oauth/token/accessible-resources' }, @@ -88,40 +88,18 @@ export const jiraAddCommentTool: ToolConfig { if (!params.cloudId) return undefined as any - const payload: Record = { - body: { - type: 'doc', - version: 1, - content: [ - { - type: 'paragraph', - content: [{ type: 'text', text: params.body ?? '' }], - }, - ], - }, - } + const payload: Record = { body: toAdf(params.body ?? '') } if (params.visibility) payload.visibility = params.visibility return payload }, }, transformResponse: async (response: Response, params?: JiraAddCommentParams) => { - const payload: Record = { - body: { - type: 'doc', - version: 1, - content: [ - { - type: 'paragraph', - content: [{ type: 'text', text: params?.body ?? '' }], - }, - ], - }, - } + const payload: Record = { body: toAdf(params?.body ?? '') } if (params?.visibility) payload.visibility = params.visibility const makeRequest = async (cloudId: string) => { - const commentUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}/comment` + const commentUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey?.trim() ?? ''}/comment` const commentResponse = await fetch(commentUrl, { method: 'POST', headers: { diff --git a/apps/sim/tools/jira/add_watcher.ts b/apps/sim/tools/jira/add_watcher.ts index aa44f13df55..65419b59be6 100644 --- a/apps/sim/tools/jira/add_watcher.ts +++ b/apps/sim/tools/jira/add_watcher.ts @@ -51,7 +51,7 @@ export const jiraAddWatcherTool: ToolConfig { if (params.cloudId) { - return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/watchers` + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}/watchers` } return 'https://api.atlassian.com/oauth/token/accessible-resources' }, @@ -65,14 +65,20 @@ export const jiraAddWatcherTool: ToolConfig { if (!params.cloudId) return undefined as any + if (!params.accountId) { + throw new Error('accountId is required to add a Jira watcher') + } return params.accountId as any }, }, transformResponse: async (response: Response, params?: JiraAddWatcherParams) => { + if (!params?.accountId) { + throw new Error('accountId is required to add a Jira watcher') + } if (!params?.cloudId) { const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) - const watcherUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}/watchers` + const watcherUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey?.trim() ?? ''}/watchers` const watcherResponse = await fetch(watcherUrl, { method: 'POST', headers: { diff --git a/apps/sim/tools/jira/add_worklog.ts b/apps/sim/tools/jira/add_worklog.ts index c7cc869ef55..8a92287907b 100644 --- a/apps/sim/tools/jira/add_worklog.ts +++ b/apps/sim/tools/jira/add_worklog.ts @@ -1,29 +1,25 @@ import type { JiraAddWorklogParams, JiraAddWorklogResponse } from '@/tools/jira/types' import { SUCCESS_OUTPUT, TIMESTAMP_OUTPUT, USER_OUTPUT_PROPERTIES } from '@/tools/jira/types' -import { getJiraCloudId, transformUser } from '@/tools/jira/utils' +import { + getJiraCloudId, + normalizeJiraWorklogTimestamp, + toAdf, + transformUser, +} from '@/tools/jira/utils' import type { ToolConfig } from '@/tools/types' /** * Builds the worklog request body per Jira API v3. */ function buildWorklogBody(params: JiraAddWorklogParams) { + const t = Number(params.timeSpentSeconds) + if (!Number.isFinite(t) || t <= 0) { + throw new Error('timeSpentSeconds must be a positive finite number') + } const body: Record = { - timeSpentSeconds: Number(params.timeSpentSeconds), - comment: params.comment - ? { - type: 'doc', - version: 1, - content: [ - { - type: 'paragraph', - content: [{ type: 'text', text: params.comment }], - }, - ], - } - : undefined, - started: - (params.started ? params.started.replace(/Z$/, '+0000') : undefined) || - new Date().toISOString().replace(/Z$/, '+0000'), + timeSpentSeconds: t, + comment: params.comment ? toAdf(params.comment) : undefined, + started: normalizeJiraWorklogTimestamp(params.started || new Date().toISOString()), } if (params.visibility) body.visibility = params.visibility return body @@ -113,7 +109,7 @@ export const jiraAddWorklogTool: ToolConfig { if (params.cloudId) { - return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/worklog` + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}/worklog` } return 'https://api.atlassian.com/oauth/token/accessible-resources' }, @@ -132,12 +128,13 @@ export const jiraAddWorklogTool: ToolConfig { - if (!params?.timeSpentSeconds || params.timeSpentSeconds <= 0) { - throw new Error('timeSpentSeconds is required and must be greater than 0') + const t = Number(params?.timeSpentSeconds) + if (!params?.timeSpentSeconds || !Number.isFinite(t) || t <= 0) { + throw new Error('timeSpentSeconds is required and must be a positive finite number') } const makeRequest = async (cloudId: string) => { - const worklogUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}/worklog` + const worklogUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey?.trim() ?? ''}/worklog` const worklogResponse = await fetch(worklogUrl, { method: 'POST', headers: { diff --git a/apps/sim/tools/jira/assign_issue.ts b/apps/sim/tools/jira/assign_issue.ts index e1b0e228a27..7312a006883 100644 --- a/apps/sim/tools/jira/assign_issue.ts +++ b/apps/sim/tools/jira/assign_issue.ts @@ -3,6 +3,20 @@ import { SUCCESS_OUTPUT, TIMESTAMP_OUTPUT } from '@/tools/jira/types' import { getJiraCloudId } from '@/tools/jira/utils' import type { ToolConfig } from '@/tools/types' +/** + * Maps user-provided accountId to the Jira API value. + * Empty string, "null", "none", or "unassigned" → null (unassign). + * "-1" → "-1" (auto-assign). Otherwise the trimmed accountId. + */ +function resolveAssigneeAccountId(value: string | null | undefined): string | null { + if (value === null || value === undefined) return null + const trimmed = String(value).trim() + if (trimmed === '') return null + const lower = trimmed.toLowerCase() + if (lower === 'null' || lower === 'none' || lower === 'unassigned') return null + return trimmed +} + export const jiraAssignIssueTool: ToolConfig = { id: 'jira_assign_issue', name: 'Jira Assign Issue', @@ -38,7 +52,7 @@ export const jiraAssignIssueTool: ToolConfig { if (params.cloudId) { - return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/assignee` + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}/assignee` } return 'https://api.atlassian.com/oauth/token/accessible-resources' }, @@ -66,16 +80,14 @@ export const jiraAssignIssueTool: ToolConfig { if (!params.cloudId) return undefined as any - return { - accountId: params.accountId === 'null' ? null : params.accountId, - } + return { accountId: resolveAssigneeAccountId(params.accountId) } }, }, transformResponse: async (response: Response, params?: JiraAssignIssueParams) => { if (!params?.cloudId) { const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) - const assignUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}/assignee` + const assignUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey?.trim() ?? ''}/assignee` const assignResponse = await fetch(assignUrl, { method: 'PUT', headers: { @@ -83,9 +95,7 @@ export const jiraAssignIssueTool: ToolConfig = { @@ -71,19 +71,29 @@ export const jiraBulkRetrieveTool: ToolConfig { if (params?.cloudId) return params.cloudId const accessibleResources = await response.json() - const normalizedInput = `https://${params?.domain}`.toLowerCase() + if (!Array.isArray(accessibleResources) || accessibleResources.length === 0) { + throw new Error('No Jira resources found') + } + const normalizedInput = normalizeDomain(params?.domain ?? '') const matchedResource = accessibleResources.find( - (r: any) => r.url.toLowerCase() === normalizedInput + (r: { url: string }) => r.url.toLowerCase().replace(/\/+$/, '') === normalizedInput ) if (matchedResource) return matchedResource.id - if (Array.isArray(accessibleResources) && accessibleResources.length > 0) - return accessibleResources[0].id - throw new Error('No Jira resources found') + if (accessibleResources.length === 1) return accessibleResources[0].id + throw new Error( + `Could not match Jira domain "${params?.domain}" to any accessible resource. ` + + `Available sites: ${accessibleResources.map((r: { url: string }) => r.url).join(', ')}` + ) } const cloudId = await resolveCloudId() const projectKey = await resolveProjectKey(cloudId, params!.accessToken, params!.projectId) - const jql = `project = ${projectKey} ORDER BY updated DESC` + if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(projectKey)) { + throw new Error( + `Invalid Jira project key "${projectKey}". Expected an alphanumeric project key (e.g., PROJ).` + ) + } + const jql = `project = "${projectKey}" ORDER BY updated DESC` let collected: any[] = [] let nextPageToken: string | undefined diff --git a/apps/sim/tools/jira/create_issue_link.ts b/apps/sim/tools/jira/create_issue_link.ts index 86c087419d8..aabc56cdd2d 100644 --- a/apps/sim/tools/jira/create_issue_link.ts +++ b/apps/sim/tools/jira/create_issue_link.ts @@ -1,6 +1,6 @@ import type { JiraCreateIssueLinkParams, JiraCreateIssueLinkResponse } from '@/tools/jira/types' import { SUCCESS_OUTPUT, TIMESTAMP_OUTPUT } from '@/tools/jira/types' -import { getJiraCloudId } from '@/tools/jira/utils' +import { getJiraCloudId, toAdf } from '@/tools/jira/utils' import type { ToolConfig } from '@/tools/types' export const jiraCreateIssueLinkTool: ToolConfig< @@ -124,27 +124,9 @@ export const jiraCreateIssueLinkTool: ToolConfig< }, body: JSON.stringify({ type: resolvedType, - inwardIssue: { key: params!.inwardIssueKey }, - outwardIssue: { key: params!.outwardIssueKey }, - comment: params?.comment - ? { - body: { - type: 'doc', - version: 1, - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: params!.comment, - }, - ], - }, - ], - }, - } - : undefined, + inwardIssue: { key: params!.inwardIssueKey?.trim() ?? '' }, + outwardIssue: { key: params!.outwardIssueKey?.trim() ?? '' }, + comment: params?.comment ? { body: toAdf(params.comment) } : undefined, }), }) if (!linkResponse.ok) { diff --git a/apps/sim/tools/jira/delete_attachment.ts b/apps/sim/tools/jira/delete_attachment.ts index 58448b0213d..62f9b6afbb9 100644 --- a/apps/sim/tools/jira/delete_attachment.ts +++ b/apps/sim/tools/jira/delete_attachment.ts @@ -48,7 +48,7 @@ export const jiraDeleteAttachmentTool: ToolConfig< request: { url: (params: JiraDeleteAttachmentParams) => { if (params.cloudId) { - return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/attachment/${params.attachmentId}` + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/attachment/${params.attachmentId?.trim() ?? ''}` } return 'https://api.atlassian.com/oauth/token/accessible-resources' }, @@ -65,7 +65,7 @@ export const jiraDeleteAttachmentTool: ToolConfig< if (!params?.cloudId) { const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) // Make the actual request with the resolved cloudId - const attachmentUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/attachment/${params?.attachmentId}` + const attachmentUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/attachment/${params?.attachmentId?.trim() ?? ''}` const attachmentResponse = await fetch(attachmentUrl, { method: 'DELETE', headers: { diff --git a/apps/sim/tools/jira/delete_comment.ts b/apps/sim/tools/jira/delete_comment.ts index 260efc2d436..526f38c98ef 100644 --- a/apps/sim/tools/jira/delete_comment.ts +++ b/apps/sim/tools/jira/delete_comment.ts @@ -52,7 +52,7 @@ export const jiraDeleteCommentTool: ToolConfig { if (params.cloudId) { - return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/comment/${params.commentId}` + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}/comment/${params.commentId?.trim() ?? ''}` } return 'https://api.atlassian.com/oauth/token/accessible-resources' }, @@ -69,7 +69,7 @@ export const jiraDeleteCommentTool: ToolConfig { if (params.cloudId) { const deleteSubtasksParam = params.deleteSubtasks ? '?deleteSubtasks=true' : '' - return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}${deleteSubtasksParam}` + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}${deleteSubtasksParam}` } return 'https://api.atlassian.com/oauth/token/accessible-resources' }, @@ -70,7 +70,7 @@ export const jiraDeleteIssueTool: ToolConfig { if (params.cloudId) { - return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issueLink/${params.linkId}` + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issueLink/${params.linkId?.trim() ?? ''}` } return 'https://api.atlassian.com/oauth/token/accessible-resources' }, @@ -64,7 +64,7 @@ export const jiraDeleteIssueLinkTool: ToolConfig< transformResponse: async (response: Response, params?: JiraDeleteIssueLinkParams) => { if (!params?.cloudId) { const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) - const issueLinkUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issueLink/${params!.linkId}` + const issueLinkUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issueLink/${params!.linkId?.trim() ?? ''}` const issueLinkResponse = await fetch(issueLinkUrl, { method: 'DELETE', headers: { diff --git a/apps/sim/tools/jira/delete_worklog.ts b/apps/sim/tools/jira/delete_worklog.ts index 7ad56aeaf59..b43faecb00a 100644 --- a/apps/sim/tools/jira/delete_worklog.ts +++ b/apps/sim/tools/jira/delete_worklog.ts @@ -52,7 +52,7 @@ export const jiraDeleteWorklogTool: ToolConfig { if (params.cloudId) { - return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/worklog/${params.worklogId}` + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}/worklog/${params.worklogId?.trim() ?? ''}` } return 'https://api.atlassian.com/oauth/token/accessible-resources' }, @@ -68,7 +68,7 @@ export const jiraDeleteWorklogTool: ToolConfig { if (!params?.cloudId) { const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) - const worklogUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}/worklog/${params!.worklogId}` + const worklogUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey?.trim() ?? ''}/worklog/${params!.worklogId?.trim() ?? ''}` const worklogResponse = await fetch(worklogUrl, { method: 'DELETE', headers: { diff --git a/apps/sim/tools/jira/get_attachments.ts b/apps/sim/tools/jira/get_attachments.ts index 75d67125d8f..2bcc027d18d 100644 --- a/apps/sim/tools/jira/get_attachments.ts +++ b/apps/sim/tools/jira/get_attachments.ts @@ -72,7 +72,7 @@ export const jiraGetAttachmentsTool: ToolConfig< request: { url: (params: JiraGetAttachmentsParams) => { if (params.cloudId) { - return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}?fields=attachment` + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}?fields=attachment` } return 'https://api.atlassian.com/oauth/token/accessible-resources' }, @@ -87,7 +87,7 @@ export const jiraGetAttachmentsTool: ToolConfig< transformResponse: async (response: Response, params?: JiraGetAttachmentsParams) => { const fetchAttachments = async (cloudId: string) => { - const attachmentsUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}?fields=attachment` + const attachmentsUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey?.trim() ?? ''}?fields=attachment` const attachmentsResponse = await fetch(attachmentsUrl, { method: 'GET', headers: { diff --git a/apps/sim/tools/jira/get_comments.ts b/apps/sim/tools/jira/get_comments.ts index a043b13bbdb..6e2876f5768 100644 --- a/apps/sim/tools/jira/get_comments.ts +++ b/apps/sim/tools/jira/get_comments.ts @@ -85,7 +85,7 @@ export const jiraGetCommentsTool: ToolConfig { const startAt = params?.startAt ?? 0 const maxResults = params?.maxResults ?? 50 - const worklogsUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}/worklog?startAt=${startAt}&maxResults=${maxResults}` + const worklogsUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey?.trim() ?? ''}/worklog?startAt=${startAt}&maxResults=${maxResults}` const worklogsResponse = await fetch(worklogsUrl, { method: 'GET', headers: { diff --git a/apps/sim/tools/jira/remove_watcher.ts b/apps/sim/tools/jira/remove_watcher.ts index d33d19858f3..03088fef2f1 100644 --- a/apps/sim/tools/jira/remove_watcher.ts +++ b/apps/sim/tools/jira/remove_watcher.ts @@ -52,7 +52,7 @@ export const jiraRemoveWatcherTool: ToolConfig { if (params.cloudId) { - return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/watchers?accountId=${params.accountId}` + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}/watchers?accountId=${encodeURIComponent(params.accountId?.trim() ?? '')}` } return 'https://api.atlassian.com/oauth/token/accessible-resources' }, @@ -68,7 +68,7 @@ export const jiraRemoveWatcherTool: ToolConfig { if (!params?.cloudId) { const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) - const watcherUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}/watchers?accountId=${params!.accountId}` + const watcherUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey?.trim() ?? ''}/watchers?accountId=${encodeURIComponent(params!.accountId?.trim() ?? '')}` const watcherResponse = await fetch(watcherUrl, { method: 'DELETE', headers: { diff --git a/apps/sim/tools/jira/retrieve.ts b/apps/sim/tools/jira/retrieve.ts index af5e30a0972..a1ad0c30c55 100644 --- a/apps/sim/tools/jira/retrieve.ts +++ b/apps/sim/tools/jira/retrieve.ts @@ -232,7 +232,7 @@ export const jiraRetrieveTool: ToolConfig { if (params.cloudId) { - return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}?expand=renderedFields,names,schema,transitions,operations,editmeta,changelog,versionedRepresentations` + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}?expand=renderedFields,names,schema,transitions,operations,editmeta,changelog,versionedRepresentations` } return 'https://api.atlassian.com/oauth/token/accessible-resources' }, @@ -251,7 +251,7 @@ export const jiraRetrieveTool: ToolConfig { - const issueUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params.issueKey}?expand=renderedFields,names,schema,transitions,operations,editmeta,changelog,versionedRepresentations` + const issueUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}?expand=renderedFields,names,schema,transitions,operations,editmeta,changelog,versionedRepresentations` const issueResponse = await fetch(issueUrl, { method: 'GET', headers: { @@ -273,7 +273,7 @@ export const jiraRetrieveTool: ToolConfig { - const base = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params.issueKey}` + const base = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}` const [commentsResp, worklogResp, watchersResp] = await Promise.all([ fetch(`${base}/comment?maxResults=100&orderBy=-created`, { headers: { Accept: 'application/json', Authorization: `Bearer ${params.accessToken}` }, diff --git a/apps/sim/tools/jira/search_issues.ts b/apps/sim/tools/jira/search_issues.ts index 364184b9902..456ce727aaf 100644 --- a/apps/sim/tools/jira/search_issues.ts +++ b/apps/sim/tools/jira/search_issues.ts @@ -117,8 +117,7 @@ export const jiraSearchIssuesTool: ToolConfig { if (params.cloudId) { - return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/transitions` + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}/transitions` } return 'https://api.atlassian.com/oauth/token/accessible-resources' }, @@ -88,7 +88,7 @@ export const jiraTransitionIssueTool: ToolConfig< transformResponse: async (response: Response, params?: JiraTransitionIssueParams) => { const performTransition = async (cloudId: string) => { // First, fetch available transitions to get the name and target status - const transitionsUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}/transitions` + const transitionsUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey?.trim() ?? ''}/transitions` const transitionsResp = await fetch(transitionsUrl, { method: 'GET', headers: { @@ -158,7 +158,7 @@ export const jiraTransitionIssueTool: ToolConfig< // Fetch transition metadata for the response try { - const transitionsUrl = `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/transitions` + const transitionsUrl = `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}/transitions` const transitionsResp = await fetch(transitionsUrl, { method: 'GET', headers: { @@ -229,22 +229,7 @@ function buildTransitionBody(params: JiraTransitionIssueParams) { if (params.comment) { body.update = { - comment: [ - { - add: { - body: { - type: 'doc', - version: 1, - content: [ - { - type: 'paragraph', - content: [{ type: 'text', text: params.comment }], - }, - ], - }, - }, - }, - ], + comment: [{ add: { body: toAdf(params.comment) } }], } } diff --git a/apps/sim/tools/jira/types.ts b/apps/sim/tools/jira/types.ts index 74d98758196..d18481842b4 100644 --- a/apps/sim/tools/jira/types.ts +++ b/apps/sim/tools/jira/types.ts @@ -806,30 +806,30 @@ export interface JiraRetrieveResponse extends ToolResponse { assignee: { accountId: string displayName: string - active?: boolean - emailAddress?: string - avatarUrl?: string - accountType?: string - timeZone?: string + active: boolean | null + emailAddress: string | null + avatarUrl: string | null + accountType: string | null + timeZone: string | null } | null assigneeName: string | null reporter: { accountId: string displayName: string - active?: boolean - emailAddress?: string - avatarUrl?: string - accountType?: string - timeZone?: string + active: boolean | null + emailAddress: string | null + avatarUrl: string | null + accountType: string | null + timeZone: string | null } | null creator: { accountId: string displayName: string - active?: boolean - emailAddress?: string - avatarUrl?: string - accountType?: string - timeZone?: string + active: boolean | null + emailAddress: string | null + avatarUrl: string | null + accountType: string | null + timeZone: string | null } | null labels: string[] components: Array<{ id: string; name: string; description?: string }> @@ -869,21 +869,21 @@ export interface JiraRetrieveResponse extends ToolResponse { author: { accountId: string displayName: string - active?: boolean - emailAddress?: string - avatarUrl?: string - accountType?: string - timeZone?: string + active: boolean | null + emailAddress: string | null + avatarUrl: string | null + accountType: string | null + timeZone: string | null } | null authorName: string updateAuthor?: { accountId: string displayName: string - active?: boolean - emailAddress?: string - avatarUrl?: string - accountType?: string - timeZone?: string + active: boolean | null + emailAddress: string | null + avatarUrl: string | null + accountType: string | null + timeZone: string | null } | null created: string updated: string @@ -894,21 +894,21 @@ export interface JiraRetrieveResponse extends ToolResponse { author: { accountId: string displayName: string - active?: boolean - emailAddress?: string - avatarUrl?: string - accountType?: string - timeZone?: string + active: boolean | null + emailAddress: string | null + avatarUrl: string | null + accountType: string | null + timeZone: string | null } | null authorName: string updateAuthor?: { accountId: string displayName: string - active?: boolean - emailAddress?: string - avatarUrl?: string - accountType?: string - timeZone?: string + active: boolean | null + emailAddress: string | null + avatarUrl: string | null + accountType: string | null + timeZone: string | null } | null comment?: string | null started: string @@ -927,11 +927,11 @@ export interface JiraRetrieveResponse extends ToolResponse { author: { accountId: string displayName: string - active?: boolean - emailAddress?: string - avatarUrl?: string - accountType?: string - timeZone?: string + active: boolean | null + emailAddress: string | null + avatarUrl: string | null + accountType: string | null + timeZone: string | null } | null authorName: string created: string @@ -1149,21 +1149,21 @@ export interface JiraSearchIssuesResponse extends ToolResponse { assignee: { accountId: string displayName: string - active?: boolean - emailAddress?: string - avatarUrl?: string - accountType?: string - timeZone?: string + active: boolean | null + emailAddress: string | null + avatarUrl: string | null + accountType: string | null + timeZone: string | null } | null assigneeName: string | null reporter: { accountId: string displayName: string - active?: boolean - emailAddress?: string - avatarUrl?: string - accountType?: string - timeZone?: string + active: boolean | null + emailAddress: string | null + avatarUrl: string | null + accountType: string | null + timeZone: string | null } | null labels: string[] components: Array<{ id: string; name: string; description?: string }> @@ -1223,21 +1223,21 @@ export interface JiraGetCommentsResponse extends ToolResponse { author: { accountId: string displayName: string - active?: boolean - emailAddress?: string - avatarUrl?: string - accountType?: string - timeZone?: string + active: boolean | null + emailAddress: string | null + avatarUrl: string | null + accountType: string | null + timeZone: string | null } | null authorName: string updateAuthor: { accountId: string displayName: string - active?: boolean - emailAddress?: string - avatarUrl?: string - accountType?: string - timeZone?: string + active: boolean | null + emailAddress: string | null + avatarUrl: string | null + accountType: string | null + timeZone: string | null } | null created: string updated: string @@ -1431,20 +1431,20 @@ export interface JiraUpdateWorklogResponse extends ToolResponse { author: { accountId: string displayName: string - active?: boolean - emailAddress?: string - avatarUrl?: string - accountType?: string - timeZone?: string + active: boolean | null + emailAddress: string | null + avatarUrl: string | null + accountType: string | null + timeZone: string | null } | null updateAuthor: { accountId: string displayName: string - active?: boolean - emailAddress?: string - avatarUrl?: string - accountType?: string - timeZone?: string + active: boolean | null + emailAddress: string | null + avatarUrl: string | null + accountType: string | null + timeZone: string | null } | null started: string | null created: string | null diff --git a/apps/sim/tools/jira/update.ts b/apps/sim/tools/jira/update.ts index 47e8e26693a..2494950835d 100644 --- a/apps/sim/tools/jira/update.ts +++ b/apps/sim/tools/jira/update.ts @@ -157,7 +157,14 @@ export const jiraUpdateTool: ToolConfig = } } - const data = JSON.parse(responseText) + let data: any + try { + data = JSON.parse(responseText) + } catch { + throw new Error( + `Jira update failed (${response.status} ${response.statusText}): non-JSON response from /api/tools/jira/update` + ) + } if (data.success && data.output) { return data diff --git a/apps/sim/tools/jira/update_comment.ts b/apps/sim/tools/jira/update_comment.ts index 526e724fb4c..8e867a1999e 100644 --- a/apps/sim/tools/jira/update_comment.ts +++ b/apps/sim/tools/jira/update_comment.ts @@ -1,6 +1,6 @@ import type { JiraUpdateCommentParams, JiraUpdateCommentResponse } from '@/tools/jira/types' import { SUCCESS_OUTPUT, TIMESTAMP_OUTPUT, USER_OUTPUT_PROPERTIES } from '@/tools/jira/types' -import { extractAdfText, getJiraCloudId, transformUser } from '@/tools/jira/utils' +import { extractAdfText, getJiraCloudId, toAdf, transformUser } from '@/tools/jira/utils' import type { ToolConfig } from '@/tools/types' /** @@ -81,7 +81,7 @@ export const jiraUpdateCommentTool: ToolConfig { if (params.cloudId) { - return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/comment/${params.commentId}` + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}/comment/${params.commentId?.trim() ?? ''}` } return 'https://api.atlassian.com/oauth/token/accessible-resources' }, @@ -95,40 +95,18 @@ export const jiraUpdateCommentTool: ToolConfig { if (!params.cloudId) return undefined as any - const payload: Record = { - body: { - type: 'doc', - version: 1, - content: [ - { - type: 'paragraph', - content: [{ type: 'text', text: params.body }], - }, - ], - }, - } + const payload: Record = { body: toAdf(params.body ?? '') } if (params.visibility) payload.visibility = params.visibility return payload }, }, transformResponse: async (response: Response, params?: JiraUpdateCommentParams) => { - const payload: Record = { - body: { - type: 'doc', - version: 1, - content: [ - { - type: 'paragraph', - content: [{ type: 'text', text: params?.body ?? '' }], - }, - ], - }, - } + const payload: Record = { body: toAdf(params?.body ?? '') } if (params?.visibility) payload.visibility = params.visibility const makeRequest = async (cloudId: string) => { - const commentUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}/comment/${params!.commentId}` + const commentUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey?.trim() ?? ''}/comment/${params!.commentId?.trim() ?? ''}` const commentResponse = await fetch(commentUrl, { method: 'PUT', headers: { diff --git a/apps/sim/tools/jira/update_worklog.ts b/apps/sim/tools/jira/update_worklog.ts index 3fae9ee2da9..86a2c36630f 100644 --- a/apps/sim/tools/jira/update_worklog.ts +++ b/apps/sim/tools/jira/update_worklog.ts @@ -1,29 +1,31 @@ import type { JiraUpdateWorklogParams, JiraUpdateWorklogResponse } from '@/tools/jira/types' import { SUCCESS_OUTPUT, TIMESTAMP_OUTPUT, USER_OUTPUT_PROPERTIES } from '@/tools/jira/types' -import { extractAdfText, getJiraCloudId, transformUser } from '@/tools/jira/utils' +import { + extractAdfText, + getJiraCloudId, + normalizeJiraWorklogTimestamp, + toAdf, + transformUser, +} from '@/tools/jira/utils' import type { ToolConfig } from '@/tools/types' function buildWorklogBody(params: JiraUpdateWorklogParams) { + let timeSpentSeconds: number | undefined + if ( + params.timeSpentSeconds !== undefined && + params.timeSpentSeconds !== null && + String(params.timeSpentSeconds).trim() !== '' + ) { + const n = Number(params.timeSpentSeconds) + if (!Number.isFinite(n) || n <= 0) { + throw new Error('timeSpentSeconds must be a positive finite number') + } + timeSpentSeconds = n + } const body: Record = { - timeSpentSeconds: params.timeSpentSeconds ? Number(params.timeSpentSeconds) : undefined, - comment: params.comment - ? { - type: 'doc', - version: 1, - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: params.comment, - }, - ], - }, - ], - } - : undefined, - started: params.started ? params.started.replace(/Z$/, '+0000') : undefined, + timeSpentSeconds, + comment: params.comment ? toAdf(params.comment) : undefined, + started: params.started ? normalizeJiraWorklogTimestamp(params.started) : undefined, } if (params.visibility) body.visibility = params.visibility return body @@ -120,7 +122,7 @@ export const jiraUpdateWorklogTool: ToolConfig { if (params.cloudId) { - return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/worklog/${params.worklogId}` + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}/worklog/${params.worklogId?.trim() ?? ''}` } return 'https://api.atlassian.com/oauth/token/accessible-resources' }, @@ -141,7 +143,7 @@ export const jiraUpdateWorklogTool: ToolConfig { if (!params?.cloudId) { const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) - const worklogUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}/worklog/${params!.worklogId}` + const worklogUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey?.trim() ?? ''}/worklog/${params!.worklogId?.trim() ?? ''}` const worklogResponse = await fetch(worklogUrl, { method: 'PUT', headers: { diff --git a/apps/sim/tools/jira/utils.ts b/apps/sim/tools/jira/utils.ts index 02f5a28b0b9..145301cdfe4 100644 --- a/apps/sim/tools/jira/utils.ts +++ b/apps/sim/tools/jira/utils.ts @@ -72,11 +72,11 @@ export function extractAdfText(content: any): string | null { export function transformUser(user: any): { accountId: string displayName: string - active?: boolean - emailAddress?: string - avatarUrl?: string - accountType?: string - timeZone?: string + active: boolean | null + emailAddress: string | null + avatarUrl: string | null + accountType: string | null + timeZone: string | null } | null { if (!user) return null return { @@ -142,7 +142,21 @@ export async function downloadJiraAttachments( return downloaded } -function normalizeDomain(domain: string): string { +/** + * Normalizes an ISO timestamp into the format Jira's worklog API requires: + * `YYYY-MM-DDTHH:mm:ss.sss±HHMM` (offset without colon). Accepts trailing `Z` + * and `±HH:MM` offsets and rewrites them to `±HHMM`. If milliseconds are + * missing, `.000` is inserted before the offset. + */ +export function normalizeJiraWorklogTimestamp(value: string): string { + let s = value.trim() + s = s.replace(/Z$/i, '+0000') + s = s.replace(/([+-]\d{2}):(\d{2})$/, '$1$2') + s = s.replace(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})([+-]\d{4})$/, '$1.000$2') + return s +} + +export function normalizeDomain(domain: string): string { return `https://${domain .trim() .replace(/^https?:\/\//i, '') diff --git a/apps/sim/tools/jira/write.ts b/apps/sim/tools/jira/write.ts index db1fac87f86..82903b093ce 100644 --- a/apps/sim/tools/jira/write.ts +++ b/apps/sim/tools/jira/write.ts @@ -175,7 +175,14 @@ export const jiraWriteTool: ToolConfig = { } } - const data = JSON.parse(responseText) + let data: any + try { + data = JSON.parse(responseText) + } catch { + throw new Error( + `Jira write failed (${response.status} ${response.statusText}): non-JSON response from /api/tools/jira/write` + ) + } if (data.success && data.output) { return { diff --git a/apps/sim/tools/jsm/add_customer.ts b/apps/sim/tools/jsm/add_customer.ts index 8edd1ddf210..58c774c360a 100644 --- a/apps/sim/tools/jsm/add_customer.ts +++ b/apps/sim/tools/jsm/add_customer.ts @@ -39,16 +39,10 @@ export const jsmAddCustomerTool: ToolConfig | null + answers: JsmFormSimplifiedAnswer[] | null } } diff --git a/apps/sim/tools/slack/add_reaction.ts b/apps/sim/tools/slack/add_reaction.ts index fde6b4a062a..7ca567769da 100644 --- a/apps/sim/tools/slack/add_reaction.ts +++ b/apps/sim/tools/slack/add_reaction.ts @@ -60,9 +60,9 @@ export const slackAddReactionTool: ToolConfig ({ accessToken: params.accessToken || params.botToken, - channel: params.channel, - timestamp: params.timestamp, - name: params.name, + channel: params.channel?.trim(), + timestamp: params.timestamp?.trim(), + name: params.name?.trim(), }), }, diff --git a/apps/sim/tools/slack/canvas.ts b/apps/sim/tools/slack/canvas.ts index 51fcafb82bf..da70411c9e7 100644 --- a/apps/sim/tools/slack/canvas.ts +++ b/apps/sim/tools/slack/canvas.ts @@ -95,8 +95,6 @@ export const slackCanvasTool: ToolConfig success: false, output: { canvas_id: '', - channel: '', - title: '', }, error: data.error || 'Unknown error', } @@ -105,9 +103,7 @@ export const slackCanvasTool: ToolConfig return { success: true, output: { - canvas_id: data.canvas_id || data.id, - channel: data.channel || '', - title: data.title || '', + canvas_id: data.canvas_id ?? data.id ?? '', }, } }, diff --git a/apps/sim/tools/slack/delete_message.ts b/apps/sim/tools/slack/delete_message.ts index 3f2d3d00bf5..f2a290d93e1 100644 --- a/apps/sim/tools/slack/delete_message.ts +++ b/apps/sim/tools/slack/delete_message.ts @@ -57,8 +57,8 @@ export const slackDeleteMessageTool: ToolConfig< }), body: (params: SlackDeleteMessageParams) => ({ accessToken: params.accessToken || params.botToken, - channel: params.channel, - timestamp: params.timestamp, + channel: params.channel?.trim(), + timestamp: params.timestamp?.trim(), }), }, diff --git a/apps/sim/tools/slack/download.ts b/apps/sim/tools/slack/download.ts index 7d6179fd7c4..dd16ad64ea9 100644 --- a/apps/sim/tools/slack/download.ts +++ b/apps/sim/tools/slack/download.ts @@ -6,7 +6,7 @@ export const slackDownloadTool: ToolConfig ({ accessToken: params.accessToken || params.botToken, - channel: params.channel, + channel: params.channel?.trim(), user: params.user?.trim(), text: params.text, - thread_ts: params.threadTs || undefined, + thread_ts: params.threadTs?.trim() || undefined, blocks: typeof params.blocks === 'string' ? JSON.parse(params.blocks) : params.blocks || undefined, }), diff --git a/apps/sim/tools/slack/get_user.ts b/apps/sim/tools/slack/get_user.ts index e63915f844f..5c95b6ee349 100644 --- a/apps/sim/tools/slack/get_user.ts +++ b/apps/sim/tools/slack/get_user.ts @@ -43,7 +43,7 @@ export const slackGetUserTool: ToolConfig { const url = new URL('https://slack.com/api/users.info') - url.searchParams.append('user', params.userId) + url.searchParams.append('user', params.userId.trim()) return url.toString() }, method: 'GET', @@ -79,34 +79,38 @@ export const slackGetUserTool: ToolConfig { const url = new URL('https://slack.com/api/conversations.members') - url.searchParams.append('channel', params.channel) + url.searchParams.append('channel', params.channel.trim()) // Set limit (default 100, max 200) const limit = params.limit ? Math.min(Number(params.limit), 200) : 100 url.searchParams.append('limit', String(limit)) + const cursor = params.cursor?.trim() + if (cursor) { + url.searchParams.append('cursor', cursor) + } + return url.toString() }, method: 'GET', @@ -89,6 +100,7 @@ export const slackListMembersTool: ToolConfig ({ accessToken: params.accessToken || params.botToken, - channel: params.channel, - timestamp: params.timestamp, - name: params.name, + channel: params.channel?.trim(), + timestamp: params.timestamp?.trim(), + name: params.name?.trim(), }), }, diff --git a/apps/sim/tools/slack/types.ts b/apps/sim/tools/slack/types.ts index 8e193363fb0..31e4190d88c 100644 --- a/apps/sim/tools/slack/types.ts +++ b/apps/sim/tools/slack/types.ts @@ -400,11 +400,6 @@ export const USER_OUTPUT_PROPERTIES = { optional: true, }, is_app_user: { type: 'boolean', description: 'Whether user is an app user', optional: true }, - is_stranger: { - type: 'boolean', - description: 'Whether user is from different workspace', - optional: true, - }, deleted: { type: 'boolean', description: 'Whether the user is deactivated' }, color: { type: 'string', description: 'User color for display', optional: true }, timezone: { @@ -484,8 +479,6 @@ export const USERS_OUTPUT: OutputProperty = { */ export const CANVAS_OUTPUT_PROPERTIES = { canvas_id: { type: 'string', description: 'Unique canvas identifier' }, - channel: { type: 'string', description: 'Channel where canvas was created' }, - title: { type: 'string', description: 'Canvas title' }, } as const satisfies Record /** @@ -693,7 +686,6 @@ export interface SlackMessageParams extends SlackBaseParams { destinationType?: 'channel' | 'dm' channel?: string dmUserId?: string - userId?: string text: string threadTs?: string blocks?: string @@ -711,7 +703,6 @@ export interface SlackMessageReaderParams extends SlackBaseParams { destinationType?: 'channel' | 'dm' channel?: string dmUserId?: string - userId?: string limit?: number oldest?: string latest?: string @@ -750,16 +741,19 @@ export interface SlackListChannelsParams extends SlackBaseParams { includePrivate?: boolean excludeArchived?: boolean limit?: number + cursor?: string } export interface SlackListMembersParams extends SlackBaseParams { channel: string limit?: number + cursor?: string } export interface SlackListUsersParams extends SlackBaseParams { includeDeleted?: boolean limit?: number + cursor?: string } export interface SlackGetUserParams extends SlackBaseParams { @@ -883,8 +877,6 @@ export interface SlackMessageResponse extends ToolResponse { export interface SlackCanvasResponse extends ToolResponse { output: { canvas_id: string - channel: string - title: string } } @@ -1063,6 +1055,7 @@ export interface SlackListChannelsResponse extends ToolResponse { ids: string[] names: string[] count: number + nextCursor: string | null } } @@ -1070,11 +1063,13 @@ export interface SlackListMembersResponse extends ToolResponse { output: { members: string[] count: number + nextCursor: string | null } } export interface SlackUser { id: string + team_id?: string | null name: string real_name: string display_name: string @@ -1090,20 +1085,23 @@ export interface SlackUser { is_primary_owner?: boolean is_restricted?: boolean is_ultra_restricted?: boolean + is_app_user?: boolean deleted: boolean - timezone?: string - timezone_label?: string - timezone_offset?: number - avatar?: string - avatar_24?: string - avatar_48?: string - avatar_72?: string - avatar_192?: string - avatar_512?: string + color?: string | null + timezone?: string | null + timezone_label?: string | null + timezone_offset?: number | null + avatar?: string | null + avatar_24?: string | null + avatar_48?: string | null + avatar_72?: string | null + avatar_192?: string | null + avatar_512?: string | null status_text?: string status_emoji?: string - status_expiration?: number - updated?: number + status_expiration?: number | null + updated?: number | null + has_2fa?: boolean } export interface SlackListUsersResponse extends ToolResponse { @@ -1112,6 +1110,7 @@ export interface SlackListUsersResponse extends ToolResponse { ids: string[] names: string[] count: number + nextCursor: string | null } } diff --git a/apps/sim/tools/slack/update_message.ts b/apps/sim/tools/slack/update_message.ts index 9227d7a85bd..227423a5136 100644 --- a/apps/sim/tools/slack/update_message.ts +++ b/apps/sim/tools/slack/update_message.ts @@ -70,8 +70,8 @@ export const slackUpdateMessageTool: ToolConfig< }), body: (params: SlackUpdateMessageParams) => ({ accessToken: params.accessToken || params.botToken, - channel: params.channel, - timestamp: params.timestamp, + channel: params.channel?.trim(), + timestamp: params.timestamp?.trim(), text: params.text, blocks: typeof params.blocks === 'string' ? JSON.parse(params.blocks) : params.blocks || undefined,