import * as z from "zod" import { defineAction } from "../../../automation/actions" import { apolloAccountOptions, fromApolloWire, getApolloApi } from "../lib/api" import { APOLLO_PAGINATION_SCHEMA, APOLLO_PROVIDER_PAGINATION_SCHEMA, toApolloPagination, } from "../lib/schemas" const CREDIT_BALANCE_SCHEMA = z.looseObject({ consumed: z.number(), left_over: z.number(), limit: z.number(), }) const CREDIT_RESPONSE_SCHEMA = z.looseObject({ credit_usage_stats: z.record(z.string(), CREDIT_BALANCE_SCHEMA), current_credit_cycle: z.looseObject({ end_date: z.string().nullable(), start_date: z.string().nullable(), }), }) const NEWS_ARTICLE_PROVIDER_SCHEMA = z.looseObject({ domain: z.string().nullish(), event_categories: z.string().array().prefault([]), id: z.string(), organization_ids: z.string().array().prefault([]), published_at: z.string().nullish(), snippet: z.string().nullish(), title: z.string(), url: z.string(), }) const NEWS_RESPONSE_SCHEMA = z.looseObject({ news_articles: NEWS_ARTICLE_PROVIDER_SCHEMA.array().prefault([]), pagination: APOLLO_PROVIDER_PAGINATION_SCHEMA, }) const NOTE_PROVIDER_SCHEMA = z.looseObject({ account_id: z.string().nullish(), account_ids: z.string().array().prefault([]), calendar_event_ids: z.string().array().prefault([]), contact_id: z.string().nullish(), contact_ids: z.string().array().prefault([]), content: z.string(), conversation_id: z.string().nullish(), conversation_ids: z.string().array().prefault([]), created_at: z.string(), id: z.string(), is_org_chart_note: z.boolean().nullish(), opportunity_id: z.string().nullish(), opportunity_ids: z.string().array().prefault([]), pinned_to_top: z.boolean().nullish(), system: z.boolean().nullish(), updated_at: z.string(), user_id: z.string().nullish(), }) const NOTES_RESPONSE_SCHEMA = z.looseObject({ notes: NOTE_PROVIDER_SCHEMA.array().prefault([]), }) const NOTE_RELATION_ID_SCHEMA = z.string().trim().min(1) const REPORT_METRIC_SCHEMA = z.object({ displayName: z.string().optional(), smartDatetimeReference: z.string().trim().min(1), smartUserIdReference: z.string().trim().min(1), value: z.string().trim().min(1), }) const REPORT_GROUP_SCHEMA = z.object({ limit: z.number().int().positive().optional(), name: z.string().trim().min(1), }) const REPORT_SORT_SCHEMA = z.object({ ascending: z.boolean(), metric: REPORT_METRIC_SCHEMA.omit({ displayName: true }).optional(), }) const DATE_RANGE_SCHEMA = z.enum([ "all_time", "current_month", "current_quarter", "current_week", "current_year", "custom_range", "last_12_months", "last_2_weeks", "last_2_years", "last_30_days", "last_3_months", "last_4_quarters", "last_6_months", "last_7_days", "previous_month", "previous_quarter", "previous_week", "previous_year", "today", "yesterday", ]) /** Gets current Apollo credit balances and billing-cycle dates. */ export const getApolloCreditUsage = defineAction("Get Apollo credit usage") .describe("Gets consumed and remaining credits by Apollo credit category.") .account("apollo", apolloAccountOptions("credit_usage_stats_read")) .output( z.object({ categories: z .object({ category: z.string(), consumed: z.number(), limit: z.number(), remaining: z.number(), }) .array(), cycle: z.object({ endDate: z.string().optional(), startDate: z.string().optional(), }), }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account }) => { const result = await getApolloApi(account).request( "usage_stats/credit_usage_stats", { body: {}, responseSchema: CREDIT_RESPONSE_SCHEMA }, ) return { categories: Object.entries(result.credit_usage_stats).map( ([category, balance]) => ({ category: category.replace(/_([a-z0-9])/g, (_, character: string) => character.toUpperCase(), ), consumed: balance.consumed, limit: balance.limit, remaining: balance.left_over, }), ), cycle: { endDate: result.current_credit_cycle.end_date ?? undefined, startDate: result.current_credit_cycle.start_date ?? undefined, }, } }) /** Searches news articles associated with Apollo organizations. */ export const searchApolloNewsArticles = defineAction( "Search Apollo news articles", ) .describe("Searches company news by organization, category, and date range.") .account("apollo", apolloAccountOptions("organizations_news_articles")) .input( z.object({ categories: z.string().array().optional(), organizationIds: z.string().array().min(1), page: z.number().int().min(1).prefault(1), perPage: z.number().int().min(1).max(100).prefault(25), publishedFrom: z.iso.date().optional(), publishedTo: z.iso.date().optional(), }), ) .output( z.object({ articles: z .object({ categories: z.string().array(), domain: z.string().optional(), id: z.string(), organizationIds: z.string().array(), publishedAt: z.string().optional(), snippet: z.string().optional(), title: z.string(), url: z.string(), }) .array(), pagination: APOLLO_PAGINATION_SCHEMA, }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const result = await getApolloApi(account).request("news_articles/search", { method: "POST", query: { "categories[]": input.categories, "organization_ids[]": input.organizationIds, page: input.page, per_page: input.perPage, "published_at[max]": input.publishedTo, "published_at[min]": input.publishedFrom, }, responseSchema: NEWS_RESPONSE_SCHEMA, }) return { articles: result.news_articles.map((article) => ({ categories: article.event_categories, domain: article.domain ?? undefined, id: article.id, organizationIds: article.organization_ids, publishedAt: article.published_at ?? undefined, snippet: article.snippet ?? undefined, title: article.title, url: article.url, })), pagination: toApolloPagination(result.pagination), } }) /** Lists notes attached to Apollo CRM records. */ export const listApolloNotes = defineAction("List Apollo notes") .describe("Lists notes attached to contacts, accounts, deals, and meetings.") .account("apollo", apolloAccountOptions("notes_list")) .input( z .object({ accountId: NOTE_RELATION_ID_SCHEMA.optional(), calendarEventId: NOTE_RELATION_ID_SCHEMA.optional(), contactId: NOTE_RELATION_ID_SCHEMA.optional(), contactIds: NOTE_RELATION_ID_SCHEMA.array().min(1).optional(), conversationId: NOTE_RELATION_ID_SCHEMA.optional(), conversationIds: NOTE_RELATION_ID_SCHEMA.array().min(1).optional(), limit: z.number().int().min(1).max(100).prefault(100), opportunityId: NOTE_RELATION_ID_SCHEMA.optional(), skip: z.number().int().nonnegative().prefault(0), sortAscending: z.boolean().prefault(false), sortBy: z.enum(["createdAt", "updatedAt"]).prefault("createdAt"), startDate: z.iso.date().optional(), }) .refine( (input) => input.accountId !== undefined || input.calendarEventId !== undefined || input.contactId !== undefined || Boolean(input.contactIds?.length) || input.conversationId !== undefined || Boolean(input.conversationIds?.length) || input.opportunityId !== undefined, "Provide at least one note relation filter.", ), ) .output( z.object({ notes: z .object({ accountId: z.string().optional(), accountIds: z.string().array(), calendarEventIds: z.string().array(), contactId: z.string().optional(), contactIds: z.string().array(), content: z.string(), conversationId: z.string().optional(), conversationIds: z.string().array(), createdAt: z.string(), id: z.string(), isOrgChartNote: z.boolean().optional(), opportunityId: z.string().optional(), opportunityIds: z.string().array(), pinnedToTop: z.boolean().optional(), system: z.boolean().optional(), updatedAt: z.string(), userId: z.string().optional(), }) .array(), }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { return { notes: ( await getApolloApi(account).request("notes", { query: { account_id: input.accountId, calendar_event_id: input.calendarEventId, contact_id: input.contactId, contact_ids: input.contactIds, conversation_id: input.conversationId, conversation_ids: input.conversationIds, limit: input.limit, opportunity_id: input.opportunityId, skip: input.skip, sort_by_field: input.sortBy === "createdAt" ? "created_at" : "updated_at", sort_direction: input.sortAscending ? "asc" : "desc", start_date: input.startDate, }, responseSchema: NOTES_RESPONSE_SCHEMA, }) ).notes.map((note) => ({ accountId: note.account_id ?? undefined, accountIds: note.account_ids, calendarEventIds: note.calendar_event_ids, contactId: note.contact_id ?? undefined, contactIds: note.contact_ids, content: note.content, conversationId: note.conversation_id ?? undefined, conversationIds: note.conversation_ids, createdAt: note.created_at, id: note.id, isOrgChartNote: note.is_org_chart_note ?? undefined, opportunityId: note.opportunity_id ?? undefined, opportunityIds: note.opportunity_ids, pinnedToTop: note.pinned_to_top ?? undefined, system: note.system ?? undefined, updatedAt: note.updated_at, userId: note.user_id ?? undefined, })), } }) /** Runs an Apollo analytics report synchronously. */ export const queryApolloAnalyticsReport = defineAction( "Query Apollo analytics report", ) .describe("Runs a synchronous analytics report with metrics and dimensions.") .account("apollo", apolloAccountOptions("report_sync")) .input( z.object({ dateRange: DATE_RANGE_SCHEMA, filters: z.record(z.string(), z.json()).prefault({}), groupBy: REPORT_GROUP_SCHEMA.optional(), includeGroupTotals: z.boolean().prefault(false), includePivotTotals: z.boolean().prefault(false), metrics: REPORT_METRIC_SCHEMA.array(), minRatioDenominator: z.number().int().nonnegative().prefault(0), pivotGroupBy: REPORT_GROUP_SCHEMA.optional(), skipGroupByValues: z.string().array().max(500).optional(), sort: REPORT_SORT_SCHEMA.optional(), }), ) .output(z.object({ report: z.json() })) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { return { report: z.json().parse( fromApolloWire( await getApolloApi(account).request("reports/sync_report", { body: { date_ranges: [{ modality: input.dateRange }], filters: input.filters, group_by: input.groupBy ? [{ limit: input.groupBy.limit, name: input.groupBy.name }] : [], group_by_totals_selected: input.includeGroupTotals, metrics: input.metrics.map(toReportMetric), min_ratio_denominator: input.minRatioDenominator, pivot_group_by: input.pivotGroupBy ? [ { limit: input.pivotGroupBy.limit, name: input.pivotGroupBy.name, }, ] : [], pivot_group_by_totals_selected: input.includePivotTotals, skip_group_by_values: input.skipGroupByValues, sorts: input.sort ? [ { asc: input.sort.ascending, metric: input.sort.metric ? toReportMetric(input.sort.metric) : undefined, }, ] : [], }, responseSchema: z.json(), }), ), ), } }) /** * Converts one public report metric to Apollo's provider shape. * * @param metric - Public report metric. */ function toReportMetric(metric: z.output) { return { display_name: metric.displayName, smart_datetime_reference: metric.smartDatetimeReference, smart_user_id_reference: metric.smartUserIdReference, value: metric.value, } }