import * as z from "zod" import { defineAction } from "../../../automation/actions" import { integrationScope as scope } from "../../../automation/integrations" import { apolloAccountOptions, getApolloApi } from "../lib/api" import { resolveApolloContact, resolveApolloReferences, resolveApolloTask, resolveApolloUser, } from "../lib/references" import { APOLLO_PAGINATION_SCHEMA, APOLLO_PROVIDER_TASK_SCHEMA, APOLLO_REFERENCE_SCHEMA, APOLLO_TASK_SCHEMA, toApolloPagination, toApolloTask, } from "../lib/schemas" const TASK_TYPE_SCHEMA = z.enum([ "action_item", "call", "linkedin_step_connect", "linkedin_step_interact_post", "linkedin_step_message", "linkedin_step_view_profile", "outreach_manual_email", ]) const TASK_PRIORITY_SCHEMA = z.enum(["high", "low", "medium"]) const TASK_STATUS_SCHEMA = z.enum(["completed", "scheduled", "skipped"]) const TASK_RESPONSE_SCHEMA = z.looseObject({ task: APOLLO_PROVIDER_TASK_SCHEMA, }) const TASKS_RESPONSE_SCHEMA = z.looseObject({ tasks: APOLLO_PROVIDER_TASK_SCHEMA.array().prefault([]), }) const TASK_SEARCH_RESPONSE_SCHEMA = TASKS_RESPONSE_SCHEMA.extend({ pagination: z.looseObject({ page: z.number().int(), per_page: z.number().int(), total_entries: z.number().int(), total_pages: z.number().int(), }), }) const TASK_PAGE_SCHEMA = z.object({ /** Pagination metadata for requesting another page. */ pagination: APOLLO_PAGINATION_SCHEMA, /** Tasks returned on this page. */ tasks: APOLLO_TASK_SCHEMA.array(), }) const TASK_CREATION_FIELDS_SCHEMA = z.object({ /** Contact ID, exact email/name, or explicit ID reference. */ contact: APOLLO_REFERENCE_SCHEMA, /** Full task due time. */ dueAt: z.union([z.date(), z.iso.datetime({ offset: true })]), /** Helpful task context or instructions. */ note: z.string().optional(), /** Task priority. */ priority: TASK_PRIORITY_SCHEMA.prefault("medium"), /** Initial task state. */ status: TASK_STATUS_SCHEMA.prefault("scheduled"), /** Human-readable task title. */ title: z.string().trim().min(1).optional(), /** Action the assigned user should take. */ type: TASK_TYPE_SCHEMA, /** Assigned user ID, exact email/name, or explicit ID reference. */ user: APOLLO_REFERENCE_SCHEMA, }) const UPDATE_TASK_INPUT_SCHEMA = z .object({ /** Call script or talking points for call tasks. */ callScript: z.string().optional(), /** Contact ID, exact email/name, or explicit ID reference. */ contact: APOLLO_REFERENCE_SCHEMA.optional(), /** Creator ID, exact email/name, or explicit ID reference. */ creator: APOLLO_REFERENCE_SCHEMA.optional(), /** Full task due time. */ dueAt: z.union([z.date(), z.iso.datetime({ offset: true })]).optional(), /** Helpful task context or instructions. */ note: z.string().optional(), /** Task priority. */ priority: TASK_PRIORITY_SCHEMA.optional(), /** Team-defined relevant field names to surface. */ relevantFields: z.string().trim().min(1).array().optional(), /** Task ID, exact title, or explicit ID reference. */ task: APOLLO_REFERENCE_SCHEMA, /** Human-readable task title. */ title: z.string().trim().min(1).optional(), /** Action the assigned user should take. */ type: TASK_TYPE_SCHEMA.optional(), /** Assigned user ID, exact email/name, or explicit ID reference. */ user: APOLLO_REFERENCE_SCHEMA.optional(), }) .refine(hasTaskMutation, "Provide at least one task field to update.") /** Searches Apollo tasks with explicit pagination. */ export const searchApolloTasks = defineAction("Search Apollo tasks") .describe("Searches workspace tasks without hiding Apollo pagination.") .account("apollo", apolloAccountOptions("tasks_list")) .input( z.object({ /** Provider open-factor names used to filter task queues. */ openFactorNames: z.string().trim().min(1).array().optional(), /** One-indexed result page. */ page: z.number().int().min(1).prefault(1), /** Maximum tasks returned per page. */ perPage: z.number().int().min(1).max(100).prefault(100), /** Provider-native task field used for sorting. */ sortBy: z.string().trim().min(1).optional(), }), ) .output(TASK_PAGE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const result = await getApolloApi(account).request("tasks/search", { method: "POST", query: { "open_factor_names[]": input.openFactorNames, page: input.page, per_page: input.perPage, sort_by_field: input.sortBy, }, responseSchema: TASK_SEARCH_RESPONSE_SCHEMA, }) return { pagination: toApolloPagination(result.pagination), tasks: result.tasks.map(toApolloTask), } }) /** Gets one Apollo task by ID or exact title. */ export const getApolloTask = defineAction("Get Apollo task") .describe("Gets one task using an ID, exact title, or explicit ID reference.") .account("apollo", apolloAccountOptions("tasks_list")) .input(z.object({ task: APOLLO_REFERENCE_SCHEMA })) .output(APOLLO_TASK_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const api = getApolloApi(account) return toApolloTask( ( await api.request( `tasks/${encodeURIComponent(await resolveApolloTask(api, input.task))}`, { responseSchema: TASK_RESPONSE_SCHEMA }, ) ).task, ) }) /** Creates an Apollo task for one contact. */ export const createApolloTask = defineAction("Create Apollo task") .describe("Creates a scheduled, completed, or skipped task for one contact.") .account( "apollo", apolloAccountOptions( scope.and( "tasks_create", "contact_read", "contacts_search", "users_list", ), ), ) .input(TASK_CREATION_FIELDS_SCHEMA) .output(APOLLO_TASK_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const api = getApolloApi(account) const [contactId, userId] = await Promise.all([ resolveApolloContact(api, input.contact), resolveApolloUser(api, input.user), ]) return toApolloTask( ( await api.request("tasks", { body: { contact_id: contactId, due_at: toIsoString(input.dueAt), note: input.note, priority: input.priority, status: input.status, title: input.title, type: input.type, user_id: userId, }, responseSchema: TASK_RESPONSE_SCHEMA, }) ).task, ) }) /** Creates one Apollo task for each of several contacts. */ export const bulkCreateApolloTasks = defineAction("Bulk create Apollo tasks") .describe("Creates the same task for multiple contacts in one request.") .account( "apollo", apolloAccountOptions( scope.and( "tasks_create", "contact_read", "contacts_search", "users_list", ), ), ) .input( z.object({ /** Contact IDs, exact emails/names, or explicit ID references. */ contacts: APOLLO_REFERENCE_SCHEMA.array().min(1), /** Full task due time. */ dueAt: z.union([z.date(), z.iso.datetime({ offset: true })]), /** Helpful task context or instructions. */ note: z.string().optional(), /** Task priority. */ priority: TASK_PRIORITY_SCHEMA.prefault("medium"), /** Initial task state. */ status: TASK_STATUS_SCHEMA.prefault("scheduled"), /** Action the assigned user should take. */ type: TASK_TYPE_SCHEMA, /** Assigned user ID, exact email/name, or explicit ID reference. */ user: APOLLO_REFERENCE_SCHEMA, }), ) .output(APOLLO_TASK_SCHEMA.array()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const api = getApolloApi(account) const [contactIds, userId] = await Promise.all([ resolveApolloReferences(input.contacts, (reference) => resolveApolloContact(api, reference), ), resolveApolloUser(api, input.user), ]) return ( await api.request("tasks/bulk_create", { body: { contact_ids: contactIds, due_at: toIsoString(input.dueAt), note: input.note, priority: input.priority, status: input.status, type: input.type, user_id: userId, }, responseSchema: TASKS_RESPONSE_SCHEMA, }) ).tasks.map(toApolloTask) }) /** Updates selected fields on one Apollo task. */ export const updateApolloTask = defineAction("Update Apollo task") .describe("Updates a task using semantic task, contact, and user references.") .account( "apollo", apolloAccountOptions( scope.and( "tasks_create", "tasks_list", "contact_read", "contacts_search", "users_list", ), ), ) .input(UPDATE_TASK_INPUT_SCHEMA) .output(APOLLO_TASK_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const api = getApolloApi(account) const [taskId, contactId, creatorId, userId] = await Promise.all([ resolveApolloTask(api, input.task), input.contact ? resolveApolloContact(api, input.contact) : undefined, input.creator ? resolveApolloUser(api, input.creator) : undefined, input.user ? resolveApolloUser(api, input.user) : undefined, ]) const currentTask = ( await api.request(`tasks/${encodeURIComponent(taskId)}`, { responseSchema: TASK_RESPONSE_SCHEMA, }) ).task const preservedCreatorId = creatorId ?? currentTask.creator_id if (currentTask.status === "scheduled" && !preservedCreatorId) { throw new Error( "Apollo did not return the current task creator, so the task cannot be updated without resetting it.", ) } return toApolloTask( ( await api.request(`tasks/${encodeURIComponent(taskId)}`, { body: { call_script: input.callScript, contact_id: contactId, creator_id: preservedCreatorId, due_at: input.dueAt ? toIsoString(input.dueAt) : undefined, note: input.note, priority: input.priority ?? (typeof currentTask.priority === "string" ? currentTask.priority : "medium"), relevant_fields: input.relevantFields, title: input.title, type: input.type, user_id: userId, }, method: "PATCH", responseSchema: TASK_RESPONSE_SCHEMA, }) ).task, ) }) /** Completes an Apollo task and optionally records a note. */ export const completeApolloTask = defineAction("Complete Apollo task") .describe("Marks a task complete and optionally records a completion note.") .account( "apollo", apolloAccountOptions(scope.and("tasks_create", "tasks_list")), ) .input( z.object({ /** Completion note. */ note: z.string().optional(), /** Task ID, exact title, or explicit ID reference. */ task: APOLLO_REFERENCE_SCHEMA, }), ) .output(APOLLO_TASK_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const api = getApolloApi(account) return toApolloTask( ( await api.request( `tasks/${encodeURIComponent(await resolveApolloTask(api, input.task))}/complete`, { body: { note: input.note }, responseSchema: TASK_RESPONSE_SCHEMA, }, ) ).task, ) }) /** Skips an Apollo task and optionally records a reason. */ export const skipApolloTask = defineAction("Skip Apollo task") .describe("Skips a task and can request immediate task-search reindexing.") .account( "apollo", apolloAccountOptions(scope.and("tasks_create", "tasks_list")), ) .input( z.object({ /** Skip reason. */ note: z.string().optional(), /** Reindex synchronously so search reflects the skip immediately. */ reindexImmediately: z.boolean().prefault(false), /** Task ID, exact title, or explicit ID reference. */ task: APOLLO_REFERENCE_SCHEMA, }), ) .output(APOLLO_TASK_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const api = getApolloApi(account) return toApolloTask( ( await api.request( `tasks/${encodeURIComponent(await resolveApolloTask(api, input.task))}/skip`, { body: { note: input.note, on_task_page: input.reindexImmediately, }, responseSchema: TASK_RESPONSE_SCHEMA, }, ) ).task, ) }) /** * Serializes a validated date input to Apollo's ISO string. * * @param value - Validated date value. */ function toIsoString(value: Date | string) { return value instanceof Date ? value.toISOString() : value } /** * Returns whether an update contains a mutable task field. * * @param input - Parsed task update input. */ function hasTaskMutation(input: Record) { return Object.entries(input).some( ([name, value]) => name !== "task" && value !== undefined, ) }