{"openapi":"3.1.0","info":{"title":"Vruum Public API","description":"The public REST surface of the Vruum AI Sales Agent platform — the same compound-tool contract exposed over MCP. All endpoints require a bearer token (your Vruum login token or an API token).\n\nAgents integrating with Vruum should prefer the remote MCP endpoint at https://api.vruum.ai/mcp, which exposes this exact surface as tools with schemas, auth, and discovery built in. This spec describes the equivalent REST operations for clients without MCP support. Human-readable docs: https://vruum.ai/docs — agent orientation: https://vruum.ai/llms.txt","version":"1.0.0"},"servers":[{"url":"https://api.vruum.ai"}],"security":[{"bearerAuth":[]}],"paths":{"/api/outreach/review":{"get":{"tags":["outreach"],"summary":"Get Outreach Review","description":"Default tool for outreach queue triage — pending messages with full context for review.\n\nStart here when reviewing or managing the outreach queue. Returns pending\nmessages — both 'needs_draft' (unauthored, awaiting harness authoring: write\nvia manage_messages edit) and 'draft' (authored, awaiting approve/reject),\ndisambiguated by each item's `status` — enriched with: person context,\ncampaign instructions (tone rules, selling strategy, touch sequence), match\nanalysis (summary, alignment points, recommended approach), company research\nsummary, recent LinkedIn posts, conversation thread, and outreach plan state.\nOne call gives you everything needed to author, approve, or reject.\n\nUse content_length='full' for detailed review (capped at 5 items to prevent\nconnection drops from oversized payloads — use offset to paginate),\nor 'preview' (default) for quick scanning with truncated content.\n\nFilter by campaign name or ID to review a specific queue (e.g. campaign='DFW CFOs').\n\nUse message_ids to fetch specific messages by ID (comma-separated UUIDs, max 20).\nBypasses sorting/pagination — returns full context for exactly those messages.\nUseful for subagent dispatch: pull the lightweight queue first, split into batches,\nthen each subagent calls with its assigned message_ids.\n\nOperators: pass for_company parameter to review a specific client company.\n\nReturns (units in parentheses; scope = messages.status IN ('draft','needs_draft')\nfor this tenant unless message_ids is set):\n    items (list of ReviewItem, unit=messages): one entry per pending message\n        (not per person — a person with two pending messages shows up twice).\n        Each item:\n        - message_id, status ('needs_draft' = author it, 'draft' = approve it),\n          channel, subject, content, category (initial / followup /\n          reply_response), priority_score, created_at\n        - expires_at (needs_draft only): when the nightly sweep auto-rejects\n          this row if still unauthored (created_at + 14 days). Author\n          oldest-first. An expired touch shows up later as status='rejected'\n          with blank content and ai_decision_context.expired_signal — that is\n          GC, NOT an operator rejection; revive via manage_messages\n          action=regenerate, then author and edit as normal.\n        - person_id, person_name, person_title, person_company, match_score,\n          stage (the company_people stage)\n        - campaign_name, campaign_tone_instructions, campaign_selling_strategy,\n          campaign_touch_sequence (per-touch instructions)\n        - match_summary, alignment_points, recommended_approach\n        - company_summary (recent company research),\n          recent_linkedin_posts (last 3 posts; unit=posts)\n        - conversation_thread (list of outbound+inbound rows, unit=messages,\n          ordered by created_at; truncated to 200 chars when content_length='preview')\n        - outreach_plan_id (the linked plan's id — pass straight to\n          manage_outreach pause/resume/stop; no get_person_360 needed),\n          plan_status, plan_strategy, touches_completed, max_touches,\n          plan_next_action_at (when the plan's next touch fires — an\n          approved-but-deferred send happens at this time),\n          connection_status (the plan's LinkedIn connection state) (from\n          the linked outreach_plan; unit=plans)\n        - PRIORITIZE WARM ITEMS: a linkedin_message item with\n          connection_status='connected' is a follow-up to someone who\n          ACCEPTED the connection request — author/approve these before\n          any cold lane; they are the highest-EV rows in the queue.\n    total_pending (int, unit=messages): EXACT count of all pending messages\n        matching the campaign / message_ids filter for the tenant,\n        BEFORE pagination. Filters: status IN ('draft','needs_draft'),\n        user_company_id, and the campaign filter when set. Does NOT date-filter\n        — the queue is \"everything currently pending.\"\n    offset (int): echoes the request offset.\n    limit (int, unit=messages): echoes the page size. When content_length='full'\n        this is capped at 5 (regardless of the requested limit).\n    approved_pending_send_count (int, unit=messages): approved rows that\n        have NOT sent yet (they no longer show as items). Non-zero after a\n        bulk approve means those sends are SCHEDULED (per-person cooldown /\n        designed sequence timing) — they are not lost, not sent, and must\n        not be re-drafted or manually re-sent.","operationId":"get_outreach_review","security":[{"HTTPBearer":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"description":"Max messages to review (up to 200)","default":10,"title":"Limit"},"description":"Max messages to review (up to 200)"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Pagination offset","default":0,"title":"Offset"},"description":"Pagination offset"},{"name":"content_length","in":"query","required":false,"schema":{"type":"string","description":"'full' for untruncated conversation content, 'preview' (default) for 200-char truncation. When 'full', limit is auto-capped at 5 to prevent oversized responses — use offset to paginate.","default":"preview","title":"Content Length"},"description":"'full' for untruncated conversation content, 'preview' (default) for 200-char truncation. When 'full', limit is auto-capped at 5 to prevent oversized responses — use offset to paginate."},{"name":"message_ids","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated message UUIDs to fetch specific messages. Bypasses sorting/pagination — returns full context for exactly these messages. Example: 'uuid1,uuid2,uuid3'. Max 20.","title":"Message Ids"},"description":"Comma-separated message UUIDs to fetch specific messages. Bypasses sorting/pagination — returns full context for exactly these messages. Example: 'uuid1,uuid2,uuid3'. Max 20."},{"name":"campaign","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by campaign name or ID. Example: 'DFW CFOs' or a UUID. Returns only drafts for people in that campaign.","title":"Campaign"},"description":"Filter by campaign name or ID. Example: 'DFW CFOs' or a UUID. Returns only drafts for people in that campaign."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutreachReviewResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/network/warm-path":{"post":{"tags":["network"],"summary":"Find Warm Path","description":"Find the warmest introduction path from your own network to a target person or company.\n\nTarget accepts: a person UUID, a company UUID, a LinkedIn URL, an email address,\nor a free-text name (\"Jane Chen at Acme\"). Returns ranked paths (direct\nrelationship, customer/champion bridge, connector employed at the target company)\nwith named connectors, whose relationship each is, quotable evidence, a per-factor\nscore breakdown, and per-source data freshness. Scores are an ORDINAL ranking for\nprioritization, not a calibrated probability. Empty results include a reason\n(sync_disabled / sync_pending / no_match) and the next step; ambiguous free-text\ntargets return candidates to re-call with. Set include_draft=true to also get an\nintro-ask draft for the top path — drafts are never sent automatically.","operationId":"find_warm_path","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FindWarmPathRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FindWarmPathResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/people/person/{person_id}/360":{"get":{"tags":["people"],"summary":"Get Person 360","description":"Complete person context in one call.\n\nAggregates person info, match analysis, research, activity timeline,\noutreach plan state, and deal info — replacing 5 separate tool calls.","operationId":"get_person_360","security":[{"HTTPBearer":[]}],"parameters":[{"name":"person_id","in":"path","required":true,"schema":{"type":"string","title":"Person Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Person360Response"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/deals/{deal_id}/360":{"get":{"tags":["deals"],"summary":"Get Deal Compound","description":"Complete deal context in one call — replaces 3 separate tool calls.\n\nAggregates deal info, stakeholders with person details, MEDDIC qualification\nstate with gap descriptions, and recent activity timeline.\n\nLike get_person_360 but for deals. Use this as the default tool for\nunderstanding a deal's current state.\n\nOperators: use X-Company-Id header for cross-company access.","operationId":"get_deal_360","security":[{"HTTPBearer":[]}],"parameters":[{"name":"deal_id","in":"path","required":true,"schema":{"type":"string","title":"Deal Id"}},{"name":"timeline_limit","in":"query","required":false,"schema":{"type":"integer","default":30,"title":"Timeline Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/deals/pipeline/inspect":{"get":{"tags":["deals"],"summary":"Inspect Pipeline","description":"Risk-first pipeline view. Returns the 5 most at-risk deals with risk scores and factors.\n\nRisk heuristics: silence (14d=+35, 7d=+20), overdue next step (+5/day max 25),\nno next step (+15), past close date (+20), low qualification (+10), no qualification (+5),\nsingle stakeholder (+10).\n\nAlso returns pipeline health (healthy/some_risk/critical), total active deals, and total value.\nOperators: use X-Company-Id header for cross-company inspection.","operationId":"inspect_pipeline","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/analytics/performance":{"get":{"tags":["analytics"],"summary":"Get Performance Metrics","description":"Get outreach performance metrics: time-series trends and/or conversion funnel.\n\nSet view='timeseries' for daily counts of a single metric (contacts/people/replies/meetings).\nSet view='funnel' for aggregate conversion data: contacted->replied->met funnel with rates,\nreply rates by channel, and LinkedIn pipeline breakdown.\nSet view='both' for everything in one call.\n\nFor T1s sent over time: view='timeseries', metric='contacts'.\nFor reply rate and funnel: view='funnel'.\nOperators: pass for_company to get metrics for a specific client.\n\nUNIT CONVENTIONS USED IN RESPONSES\nEvery count field is one of {messages, unique_people, plans, campaigns, days}; see the\nReturns block for the unit attached to each field. Note that timeseries 'contacts' and\nfunnel.contacted are BOTH unique_people (i.e. unique people, deduped per person_id),\nNOT message counts.\n\nDATE-FILTER COLUMN PER METRIC (timeseries)\n- metric='people':    filters on company_people.created_at — start_date / end_date\n- metric='contacts':  filters on messages.sent_at (sequence_number=1 only) — start_date / end_date\n- metric='replies':   filters on company_people.replied_at — start_date / end_date\n- metric='meetings':  filters on company_people.meeting_booked_at — start_date / end_date\nNOTE: end_date is IGNORED by reply_rates_by_channel in the direct (no campaign_id)\nmode — see the `end_date` query-param description for details.\n\nReturns (selected fields, with unit + scope + date-filter annotations):\n    view (str):\n        Echoes the requested view.\n\n    timeseries (object, when view='timeseries' or 'both'):\n        metric (str): The metric name.\n        data (list of {date, count}):\n            date (str): YYYY-MM-DD.\n            count (int): unit = unique_people for ALL four metrics (T1-sent dedupe by\n            person; company_people rows for people/replies/meetings).\n\n    funnel (object, when view='funnel' or 'both'):\n        total_people (int, unit=unique_people, scope=non-archived company_people in\n            tenant, no date filter): denominator for contact_rate.\n        contacted (int, unit=unique_people, scope=non-archived; date-filter:\n            company_people.last_message_sent_at): distinct people with at least one\n            sent message in window. NOT the total message count.\n        replied (int, unit=unique_people, scope=non-archived; date-filter:\n            company_people.replied_at): distinct people who replied. NOT the total\n            reply-message count.\n        meetings (int, unit=unique_people, scope=includes archived; date-filter:\n            company_people.meeting_booked_at): a meeting is a permanent positive\n            outcome so it counts regardless of archive state.\n        summary (str): human prose summary of the funnel.\n\n    conversion_rates (object): contact_rate, reply_rate, meeting_rate, booking_rate\n        — percentages computed from the funnel counts above, NOT independent measures.\n\n    reply_rates_by_channel (list, when view='funnel' or 'both'):\n        One entry per channel actually used. Per-channel entries (unit=unique_people\n        for sent/replied/accepted):\n        - 'linkedin_connection': sent + accepted + acceptance_rate (NOT a reply_rate;\n          connection requests are top-of-funnel, replies happen on follow-up DMs).\n          note: \"Acceptance rate, not reply rate. Replies happen on follow-up messages.\"\n          CAVEAT: accepted_count is all-time (no date filter) in direct mode; sent is\n          date-filtered.\n        - 'linkedin_message': sent + replied + reply_rate (follow-up DMs sent to\n          connected people).\n        - 'email': sent + replied + reply_rate.\n        - 'linkedin_inmail': sent + replied + reply_rate.\n\n    stage_funnel (object, when view='funnel' or 'both'):\n        connection_requests_sent (int, unit=messages, scope=channel='linkedin_connection',\n            status='sent'; date-filter messages.sent_at).\n        connections_accepted (int, unit=plans, scope=outreach_plans.connection_status=\n            'connected'; NO date filter — all-time count). Cross-walk warning: numerator\n            is plans, denominator is messages; the rate is approximate when the plan\n            set spans dates outside the window.\n        acceptance_rate (float, unit=days→percent): connections_accepted /\n            connection_requests_sent.\n        followups_sent (int, unit=messages, scope=channel='linkedin_message',\n            status='sent'; date-filter messages.sent_at).\n        followup_replies (int, unit=messages, scope=channel='linkedin_message',\n            has_reply=true; date-filter messages.sent_at — i.e. the original\n            outbound's sent_at, NOT the inbound reply's receipt timestamp).\n        followup_reply_rate (float, percent): of the follow-up DMs SENT (NOT of\n            accepted connections). Reply / send within the window.\n        meetings (int, unit=unique_people): mirrors funnel.meetings.\n        reply_to_meeting_rate (float, percent): unique_people_meetings / unique_people_replied.\n        summary (str): human prose summary.\n            \"Biggest drop:\" picks the largest absolute drop among\n            (connection_requests_sent - connections_accepted),\n            (followups_sent - followup_replies),\n            (replied - meetings). This is a raw-count heuristic; it can name\n            \"connection acceptance\" even when the acceptance rate is healthy\n            (e.g. 80%) because the absolute drop is still the largest of the three.\n\n    connections (object, when view='connections'):\n        total_requested (int, unit=plans).\n        acceptance_rate (float, percent of plans).\n        avg_time_to_accept_hours (float|null).\n        withdrawal_rate (float, percent): plans auto-withdrawn / total requested.\n        retry_success_rate (float): connected-after-retry / completed-retried plans.\n        by_status (object): plan counts keyed by connection_status.","operationId":"get_performance_metrics","security":[{"HTTPBearer":[]}],"parameters":[{"name":"view","in":"query","required":false,"schema":{"type":"string","description":"What to return: 'timeseries' for daily counts of a single metric, 'funnel' for aggregate conversion data, 'both' for everything in one call.","default":"both","title":"View"},"description":"What to return: 'timeseries' for daily counts of a single metric, 'funnel' for aggregate conversion data, 'both' for everything in one call."},{"name":"metric","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Metric for timeseries view. Each value documents its own scope + date-filter column (units in parentheses): 'people' (unique_people) = new company_people rows for this tenant, filtered on company_people.created_at — i.e. imports per day; 'contacts' (unique_people) = T1 messages ONLY (messages.sequence_number=1), deduped per person, filtered on messages.sent_at — follow-up touches NOT counted (to see total send activity per day, query messages table directly); 'replies' (unique_people) = distinct company_people whose replied_at falls in window, filtered on company_people.replied_at (NOT the original message's sent_at); 'meetings' (unique_people) = distinct company_people with meeting_booked_at in window, filtered on company_people.meeting_booked_at. Required when view includes 'timeseries' or 'both'.","title":"Metric"},"description":"Metric for timeseries view. Each value documents its own scope + date-filter column (units in parentheses): 'people' (unique_people) = new company_people rows for this tenant, filtered on company_people.created_at — i.e. imports per day; 'contacts' (unique_people) = T1 messages ONLY (messages.sequence_number=1), deduped per person, filtered on messages.sent_at — follow-up touches NOT counted (to see total send activity per day, query messages table directly); 'replies' (unique_people) = distinct company_people whose replied_at falls in window, filtered on company_people.replied_at (NOT the original message's sent_at); 'meetings' (unique_people) = distinct company_people with meeting_booked_at in window, filtered on company_people.meeting_booked_at. Required when view includes 'timeseries' or 'both'."},{"name":"start_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Start date (YYYY-MM-DD), inclusive. Filters on the metric-specific timestamp column documented in `metric` for timeseries view; filters last_message_sent_at (contacted), replied_at (replied), meeting_booked_at (meetings) for funnel view. Omit for last-30-days default on timeseries; omit for all-time on funnel.","title":"Start Date"},"description":"Start date (YYYY-MM-DD), inclusive. Filters on the metric-specific timestamp column documented in `metric` for timeseries view; filters last_message_sent_at (contacted), replied_at (replied), meeting_booked_at (meetings) for funnel view. Omit for last-30-days default on timeseries; omit for all-time on funnel."},{"name":"end_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"End date (YYYY-MM-DD), inclusive on company_people queries (funnel) / inclusive via next-day exclusive bound on messages queries (timeseries 'contacts'). NOTE: end_date is currently IGNORED by reply_rates_by_channel in the no-campaign (direct) mode — the per-channel util only takes a `since` bound. Date-bounded per-channel reply rates ARE applied when campaign_id is set (uses the campaign RPC). Documented as-is; do not assume end_date capping in direct mode.","title":"End Date"},"description":"End date (YYYY-MM-DD), inclusive on company_people queries (funnel) / inclusive via next-day exclusive bound on messages queries (timeseries 'contacts'). NOTE: end_date is currently IGNORED by reply_rates_by_channel in the no-campaign (direct) mode — the per-channel util only takes a `since` bound. Date-bounded per-channel reply rates ARE applied when campaign_id is set (uses the campaign RPC). Documented as-is; do not assume end_date capping in direct mode."},{"name":"campaign_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter results to a single campaign (UUID). Applies to all funnel counts and per-channel rates; for timeseries it applies to all metrics.","title":"Campaign Id"},"description":"Filter results to a single campaign (UUID). Applies to all funnel counts and per-channel rates; for timeseries it applies to all metrics."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/marketing/engagement-review":{"get":{"tags":["marketing"],"summary":"Get Engagement Review","description":"Default tool for engagement queue triage — pending engagements with full context.\n\nStart here when reviewing warming comments, nurture reactions, or marketing\nengagements. Returns draft engagement items enriched with: person context,\ncampaign instructions (tone, selling strategy), match analysis (summary,\nalignment points, recommended approach), outreach plan state (warming/nurture\nprogress with graduation thresholds), recent engagement history, company\nresearch, budget status, and bundle info. One call gives you everything\nneeded to decide approve/skip.\n\nUse content_length='full' for detailed review (capped at 5 items),\nor 'preview' (default) for quick scanning with truncated content.\n\nFilter by source (warming/nurture/marketing) or campaign name.\n\nUse engagement_ids to fetch specific items by ID (comma-separated UUIDs,\nmax 20) for subagent batch dispatch.","operationId":"get_engagement_review","security":[{"HTTPBearer":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"description":"Max items to review (up to 200)","default":10,"title":"Limit"},"description":"Max items to review (up to 200)"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Pagination offset","default":0,"title":"Offset"},"description":"Pagination offset"},{"name":"content_length","in":"query","required":false,"schema":{"type":"string","description":"'full' for untruncated content (auto-capped at 5 items), 'preview' (default) for truncated content.","default":"preview","title":"Content Length"},"description":"'full' for untruncated content (auto-capped at 5 items), 'preview' (default) for truncated content."},{"name":"source","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by source: warming, nurture, or marketing","title":"Source"},"description":"Filter by source: warming, nurture, or marketing"},{"name":"engagement_ids","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated engagement UUIDs for direct fetch. Bypasses sorting/pagination. Max 20.","title":"Engagement Ids"},"description":"Comma-separated engagement UUIDs for direct fetch. Bypasses sorting/pagination. Max 20."},{"name":"sender_user_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by LinkedIn account user ID","title":"Sender User Id"},"description":"Filter by LinkedIn account user ID"},{"name":"campaign","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by campaign name or UUID. Returns only engagements for people in that campaign.","title":"Campaign"},"description":"Filter by campaign name or UUID. Returns only engagements for people in that campaign."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EngagementReviewResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/marketing/content-review":{"get":{"tags":["marketing"],"summary":"Get Content Review","description":"Review content post drafts with performance context and calendar awareness.\n\nReturns draft and scheduled LinkedIn posts enriched with: past performance\nstats (avg engagement metrics for same content type over last 30 days),\ncalendar neighbors (other posts within ±3 days to detect topic clustering),\ntone instructions from company settings, and a calendar summary.\n\nUse this when reviewing demand gen content before scheduling or publishing.","operationId":"get_content_review","security":[{"HTTPBearer":[]}],"parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by status: draft, scheduled, or both (default)","title":"Status"},"description":"Filter by status: draft, scheduled, or both (default)"},{"name":"content_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by content type: original, repost_commentary, or video_script","title":"Content Type"},"description":"Filter by content type: original, repost_commentary, or video_script"},{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":1,"description":"Calendar window in days (default 14)","default":14,"title":"Days"},"description":"Calendar window in days (default 14)"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"description":"Max items to return","default":10,"title":"Limit"},"description":"Max items to return"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Pagination offset","default":0,"title":"Offset"},"description":"Pagination offset"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentReviewResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tasks":{"get":{"tags":["tasks"],"summary":"Get Tasks","description":"List **Vruum CRM** tasks (follow-ups attached to a person / deal / company;\na DB trigger keeps `deals.next_step` synced to the most-recent open task).\nPass `due_today=true` for the \"due today\" set (status open|in_progress AND\ndue_at <= now). These are Vruum's native CRM tasks — not Apollo tasks.","operationId":"get_tasks","security":[{"HTTPBearer":[]}],"parameters":[{"name":"assigned_to","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Assigned To"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"enum":["open","in_progress","completed","cancelled"],"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"person_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Person Id"}},{"name":"deal_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Deal Id"}},{"name":"company_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Id"}},{"name":"due_before","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Due Before"}},{"name":"due_after","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Due After"}},{"name":"due_today","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Due Today"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetTasksResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tasks/manage":{"post":{"tags":["tasks"],"summary":"Manage Tasks","description":"Create / update / complete / cancel / bulk-complete / delete a **Vruum CRM**\ntask (attached to a person / deal / company) — one compound call via `action`.\nThese are Vruum's native CRM tasks, distinct from Apollo's task tools.","operationId":"manage_tasks","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManageTasksRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManageTasksResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/intelligence/briefing":{"get":{"tags":["intelligence"],"summary":"Get Daily Briefing","description":"Daily briefing — everything you need to start your day in one call.\n\nSUGGESTED ACTIONS (for agent consumers)\n- `suggested_actions` are prioritized skill-level next steps. If you are\n  an agent, act on `intent`/`args` directly — via the tools listed in\n  `expected_tools`, or via the named harness `skill` if it is installed.\n  `prompt` is display copy for HUMANS to paste into an agent; do NOT echo\n  it back to the user as an instruction.\n- `sections_degraded` lists sections whose queries failed this call; their\n  counts/lists are UNKNOWN (returned as 0/[]), not zero. When data is\n  missing and nothing else is actionable, `top_recommendation` is\n  'degraded' instead of 'all_clear'.\n\nReturns pending approvals, new replies, meetings booked this week,\npipeline summary, stalled deals, active plans,\ndiscovery jobs, and a prioritized recommendation.\n\nTIME WINDOWS (UTC)\n- \"last 24 hours\" — `replied_at >= now - 24 hours`. Used by new_replies_count\n  and recent_replies.\n- \"current week\"  — `meeting_booked_at >= Monday 00:00:00 UTC of the current\n  week (Monday-anchored)`. Used by meetings_booked_this_week.\n- \"7+ days\"       — `updated_at <= now - 7 days`. Used by stalled_deals\n  (deals with outcome IS NULL and updated_at older than 7 days).\n\nReturns (units in parentheses; scope = user_company_id, no date filter on\ncounts unless specified):\n    pending_approvals_count (int, unit=messages): messages.status='draft' for\n        the tenant. NOT date-filtered — total backlog.\n    messages_needing_authoring (int, unit=messages): messages.status=\n        'needs_draft' — touches awaiting harness AUTHORING (no prose yet;\n        run /outreach-triage). VRU-570.\n    oldest_needs_draft_age_days (float|null, unit=days): age of the oldest\n        unauthored engagement signal — signals TTL-dismiss at 14 days\n        (VRU-671), so a high value means the queue is rotting unauthored.\n    engagements_needing_authoring (int, unit=engagements):\n        linkedin_engagement_queue.status='needs_draft' — comments awaiting\n        authoring (run /engagement-triage). VRU-570.\n    top_pending_approvals (list, unit=messages, max 3): a preview of the most\n        recent pending drafts; each item has message_id, person_name, channel,\n        category, subject, content_preview (200-char truncation).\n    new_replies_count (int, unit=messages): messages.has_reply=true with\n        replied_at within the last 24 hours.\n    recent_replies (list, unit=messages, max 3): preview of those replies.\n    meetings_booked_this_week (int, unit=unique_people): distinct\n        company_people with meeting_booked_at since Monday 00:00 UTC. People,\n        not meetings — same person twice in one week still counts as 1.\n    pipeline_summary (list of PipelineStage, unit=deals): aggregated per-stage\n        for open deals (outcome IS NULL). Each: stage, count (unit=deals),\n        total_value (sum of estimated_value).\n    stalled_deals (list, unit=deals, max 5): open deals (outcome IS NULL) whose\n        updated_at is older than 7 days. days_stalled is calendar days since\n        updated_at.\n    deal_alerts (list of DealAlert, unit=deals): per-deal alerts surfaced from\n        DealService.get_deal_alerts (severity / detail / days).\n    deal_alerts_count (int, unit=deals): len(deal_alerts).\n    active_outreach_plans (int, unit=plans): outreach_plans.status='active'.\n        Not date-filtered.\n    discovery_jobs_in_progress (int, unit=jobs):\n        background_jobs.operation_type='person_discovery' with status IN\n        ('pending', 'processing').\n    tasks_due_today_count (int, unit=tasks): open/in_progress tasks with\n        due_at <= now (due today or overdue) — same set as\n        get_tasks(due_today=true). Committed follow-ups, incl. action\n        items captured from meetings.\n    top_tasks_due (list of TaskDue, max 5): preview of those tasks; each\n        has task_id, title, due_at, priority, person_id, deal_id.\n    top_recommendation (str): one of 'pending_approvals', 'new_replies',\n        'deal_alerts', 'tasks_due', 'stalled_deals', 'degraded',\n        'all_clear'. 'degraded' = section data is missing and nothing else\n        is actionable (see sections_degraded above). Priority:\n        approvals > replies > CRITICAL deal alerts > tasks_due > non-critical\n        deal alerts > stalled.\n    recommendation_detail (str): human prose explanation of the recommendation.","operationId":"get_daily_briefing","security":[{"HTTPBearer":[]}],"parameters":[{"name":"X-Vruum-Surface","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"'web' when called by the Vruum web app; absent for agents.","title":"X-Vruum-Surface"},"description":"'web' when called by the Vruum web app; absent for agents."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BriefingResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/intelligence/next-actions":{"get":{"tags":["intelligence"],"summary":"Get Next Actions","description":"Prioritized next-action list for the user.\n\nReturns actions sorted by priority: reply responses (P1), message\napprovals (P2), overdue follow-ups, deals, and due/overdue tasks (P3),\nunenrolled high-match prospects (P4).","operationId":"get_next_actions","security":[{"HTTPBearer":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"description":"Max actions to return","default":20,"title":"Limit"},"description":"Max actions to return"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NextActionsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/facade/search":{"post":{"tags":["facade"],"summary":"Search","description":"Polymorphic read-only search across your Vruum workspace.\n\nOne tool for every list/queue/search read. Pass `type` to pick the entity:\npeople (your pipeline — keyword `query`, batch dedup array, or list-mode\n`filters`), companies (EXTERNAL Hunter.io domain search for\ndecision-makers — requires filters.domain; 503 if Hunter.io is not\nconfigured), deals, campaigns, messages (outreach queue), engagements\n(LinkedIn engagement queue), content (LinkedIn content calendar), kb\n(knowledge-base search), skills (published skills), cta_links, warm_paths\n(open targets that have a warm intro path — see find_warm_path for one\ntarget).\n\nAll types except `companies` read internal, tenant-scoped data only.\nPagination (`limit`/`offset`) is supported for people (list mode), deals,\nmessages, and engagements; other types reject it. Results are returned\nunder `{type, results}` with each entity type's native payload shape.\n\nCampaign routing: use `type='campaigns'` here to list campaigns (newest\nfirst, compact by default — the prose-heavy AI-strategy fields come back\nnull; see filters.fields). To read one campaign by UUID with its full\nconfig, use `fetch` with `type='campaign'`. To create, update, clone,\ndelete, diagnose, or manage campaign members, use `manage_campaign`.","operationId":"search","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FacadeSearchRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/facade/fetch":{"post":{"tags":["facade"],"summary":"Fetch","description":"Fetch one entity (or a small batch) by type + id — the consolidated read tool.\n\nTypes and id semantics:\n- campaign: a campaign by UUID (array of UUIDs → per-id map). List\n  campaigns with `search` type='campaigns'; create/update/diagnose/member\n  changes with `manage_campaign`.\n- plan: a person outreach plan. SCALAR id = the plan UUID; ARRAY id =\n  PERSON UUIDs (routes to the native batch person-plans read, returning a\n  person_id → plan|null map). Note the scalar/array dimension difference.\n- conversation: full message history for a person (id = person_id).\n- person_research: cached person research (id = person UUID; array → per-id map).\n- company_research: cached company research (id = website/domain string;\n  array → native batch lookup).\n- account_state: per-account state row (id = company_id).\n- insights (subtype required): quality | pattern_tags | prompts |\n  benchmarks | improve | reply_diagnosis (id = person_id) |\n  mcp_patterns | mcp_pattern_detail (id = pattern_id).\n- scoreboard (subtype required): impact | bowtie (id = company_id;\n  filters: {window_days?}).\n- settings (subtype required): profile | automation | marketing |\n  channel_status | booking_link (no id).\n- billing: billing status for your company (no id).\n- publish_readiness: publish pre-check for a LinkedIn draft (id = post_id).\n- research_playbook: ICP + research workflow (id = optional campaign UUID).\n- job: background job status — poll after async tools like\n  people create / import_prospects / auto_fill_company_profile (id =\n  scalar job UUID; a one-element array is normalized).\n- csv_import: CSV import progress (id = the job id returned by\n  import_prospects csv_start).\n- deal: a raw deal row by UUID (for the enriched view use get_deal_360).\n- deal_alerts: at-risk deal alerts (no id).\n- post_analytics: LinkedIn post performance (id = post UUID; omit for\n  all posts).\n- seller_signals: the seller signal bundle for content drafting (id =\n  author user id, optional; filters: draft_brief/include_* flags).\n- stats (subtype required): outreach (filters: {start_date?, end_date?})\n  | plan | person | prose (filters: {days?, default 30, use 7 for a\n  weekly cut} — prose-quality trends from the outcome-linked review\n  corpus: per-surface reply outcomes + reply rate and per-gate-code\n  outcome lift (primary), plus keep/edit/kill/override mix, override\n  codes, edit-rate trend vs the prior window, rules_version\n  distribution) — aggregate stats snapshots.\n- marketing (subtype required): overview | activity (filters: {days?,\n  limit?}).\n- ads (subtype required): attribution (filters: {window_days?,\n  outreach_campaign_id?, by?}; by=creative|audience switches\n  from the cohort funnel to a per-creative / per-audience funnel) |\n  warming_review (filters: {limit?, offset?, outreach_campaign?, segment?})\n  | creative (id = ad creative UUID; returns the row + a derived\n  `terminal` verdict — failed with a probe-code error prefix means the\n  asset itself is bad, re-export and re-store; otherwise `next_call`\n  hints the poll/retry) | campaign (id = ad campaign UUID; returns the\n  row + spend_today_cents + last_sync_at) | targeting_entities (filters:\n  {facet (titles|seniorities|industries|locations|staff_count_ranges),\n  q (min 2 chars), integration_id?}; resolves display names into the\n  LinkedIn entity URNs the boost audience.facets form requires —\n  locations targeting is mandatory on every facet boost).\n- skill: a published skill body (id = skill UUID).\n\nBatch: id arrays are accepted for campaign, plan, person_research and\ncompany_research (max 100 ids). Loop batches return ``{id: payload}`` with\nper-id 4xx failures mapped to ``{id: {\"error\": detail}}``; any 5xx aborts\nthe whole call. Errors carry the same status as the underlying endpoint\nwith the detail prefixed ``\"<type> '<id>': ...\"``.","operationId":"fetch","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FacadeFetchRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/facade/manage_person":{"post":{"tags":["facade"],"summary":"Manage Person","description":"Create and manage people in your pipeline — the consolidated person writer.\n\nActions (id = person UUID unless noted):\n- create: add a person manually (no id; `payload` = the manual-person\n  fields, e.g. {name, company, title, email, linkedin_url, skip_research});\n  bulk via `items` (one payload per person).\n- update_contact: override contact fields (`payload` = contact overrides).\n- set_position: set the primary position (`payload` = position fields).\n- note: add a timeline note (`payload` = {body}).\n- log_interaction: log a manual touch that happened outside an automated\n  sequence — a call (with disposition + what was said), or an\n  email/LinkedIn/other touch made elsewhere. `payload` =\n  {interaction_kind: call|email|linkedin|meeting|other, direction?:\n  outbound|inbound, summary?, disposition? (calls only), occurred_at?\n  (ISO, backdatable), deal_id?}. Lands on the person's timeline attributed\n  to you (performed_by_user_id).\n- archive: soft-delete one or many people — stops outreach plans and\n  rejects pending drafts (id or id array; no payload). DESTRUCTIVE.\n- unarchive: restore archived people (id or id array; no payload).\n- meeting_booked: mark a meeting booked with this person (no payload).\n- save_discovered: save a researched prospect into the pipeline (no id;\n  `payload` = {person_id, campaign_id?, assessment?}); bulk via `items`.\n  Pass `assessment` ({match_score 0-100, match_summary, alignment_points?,\n  concerns?, why_now?, recommended_approach?, overall_confidence?,\n  scored_by?}) when you researched the person — it is AUTHORITATIVE and\n  the backend records it with provenance instead of re-scoring. Backend\n  floor stays mechanical: dedupe, identity (LinkedIn URL or email\n  required), per-tenant volume caps.\n- set_persona: set a contact's buying-role persona — economic_buyer /\n  decision_maker / influencer / unknown (`payload` = {persona,\n  reasoning?}). The harness classifies buying role at touch time and\n  writes it here; there is no backend classifier.\n\nBulk: `id` arrays apply the same action to each person (archive/unarchive\nroute to the native bulk endpoints; others loop with per-item status —\npartial failure is reported per item, never silent). `items` is for\ncreate-like actions, max 100 per call. Payloads are validated by the\nunderlying route's own schema, so a bad field returns that route's\nprecise 422.","operationId":"manage_person","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WriterRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/facade/manage_deal":{"post":{"tags":["facade"],"summary":"Manage Deal","description":"Create and manage deals through the pipeline — the consolidated deal writer.\n\nActions (id = deal UUID):\n- create: open a deal (no id; `payload` = {deal_name, person_id?,\n  company_id?, deal_source?, estimated_value?, stage?, notes?}); bulk via\n  `items`.\n- update: patch deal fields (`payload` = any of deal_name,\n  estimated_value, close_probability, expected_close_date, next_step,\n  notes, stage, …).\n- stage: advance/move the deal — sugar for update; `payload` must include\n  {stage}.\n- qualify: run AI MEDDIC-style qualification on the deal (no payload).\n- stakeholders: add/update/remove a deal stakeholder (`payload` =\n  {action: add|update|remove, person_id?, role?, notes?}).\n- won / lost / stalled: record the deal outcome (`payload` optional:\n  {loss_reason?, win_factors?}).\n- meeting_outcome: record how the meeting went (`payload` = the outcome\n  fields).\n- reopen: reopen a closed deal (`payload` optional: {stage}).\n\nBulk: `id` arrays apply the same action+payload per deal (server-side\nloop, per-item status — partial failure is reported per item, never\nsilent; max 100). Payloads are validated by the underlying route's own\nschema, so a bad field returns that route's precise 422.","operationId":"manage_deal","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WriterRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/facade/manage_messages":{"post":{"tags":["facade"],"summary":"Manage Messages","description":"Triage and edit outreach messages — the consolidated message writer.\n\nActions (id = message UUID unless noted):\n- approve: mark a draft READY TO SEND. Sending happens asynchronously and\n  respects the campaign's designed schedule plus a per-person cooldown —\n  approval may SCHEDULE rather than fire immediately. The response's\n  `processing_state` says which ('queued_for_send' vs 'scheduled' with\n  `send_not_before`); a scheduled message is NOT lost and must NOT be\n  re-approved or re-drafted (get_outreach_review shows the plan's\n  `plan_next_action_at` and an `approved_pending_send_count`). An id\n  ARRAY routes to the native bulk endpoint with per-message results.\n- reject: reject a draft (array → native bulk, same as approve).\n- edit: replace subject/content (`payload` = {subject?, content?}).\n  Scalar id ONLY — one call per message (an id array is rejected;\n  sending identical content to many people is not a supported flow).\n  Optional `attachments` sets the media sent with this draft\n  (linkedin_inmail / linkedin_message / email only): a list where each item\n  is a new upload {content_base64 (raw base64, no data: prefix; PNG/JPEG/GIF/\n  WebP/PDF, 10MB max), filename} OR {storage_path} to keep one the draft\n  already has (from get_outreach_review). Omit = leave unchanged, [] = remove\n  all, max 5. Attachments on any other channel are rejected.\n- regenerate: return the message to the authoring queue (status becomes\n  needs_draft, content cleared) — author the replacement at triage via\n  edit (no payload); id array loops with per-item status. Works on\n  draft, rejected, AND failed rows — regenerate is the recovery path\n  for a send the provider bounced (attachments survive; re-author,\n  then re-approve).\n- draft_reply: queue a reply for AUTHORING (creates a needs_draft\n  inbound_reply row — write it via edit) — id is the COMPANY-PERSON id\n  (the person you're talking to), not a message id.\n- draft_followup: queue a follow-up for AUTHORING (creates a\n  needs_draft followup row) — id is the COMPANY-PERSON id. Returns 409\n  `touch_already_exists` (with the existing row's id and status) when the\n  person's plan already has a live or delivered row for the resolved\n  touch — review THAT row instead of re-drafting; never loop on this call.\n- send: send a one-off email NOW to a person (id = the PERSON id, not a\n  message id; scalar only). `payload` = {sender_config_id, to_email,\n  subject, body (PLAIN TEXT — blank lines become paragraphs, newlines become\n  line breaks; HTML is escaped, do NOT pass tags), to_name?, thread_id?,\n  in_reply_to_provider_message_id?}.\n  Recorded as a manual touch (the engine won't follow up) and lands on the\n  timeline attributed to you. The backend enforces the safety floor:\n  to_email must match the person's known email, suppressed/archived\n  recipients are rejected, the mailbox must be an active one in your\n  workspace, a retried call is idempotent (no double-send), and a\n  per-person cooldown blocks a second outbound within ~20h of the last\n  one (error_code='cooldown_active' with `blocked_until` and the safe\n  next action — if it fires, the person was contacted recently and their\n  sequence is handling them; do not re-send or re-word). A deliberate\n  repeat send inside the window is possible via payload\n  {override_cooldown: true, override_reason} — owner/operator role only,\n  audit-logged. Use ONLY when the human operator explicitly asks for a\n  repeat send; NEVER decide to override on your own or in reaction to a\n  cooldown block.\n- send_linkedin: send a one-off LinkedIn touch NOW to a person (id = the\n  PERSON id, scalar only) — no campaign, no enrollment. `payload` =\n  {send_type, body, subject?, override_cooldown?, override_reason?}.\n  send_type is 'dm' (message an existing connection), 'inmail' (message a\n  non-connection, uses an InMail credit, subject REQUIRED), or 'connection'\n  (send a connection request; body is the note, truncated to the sender's\n  tier cap). Send_type is validated against the person's LIVE connection\n  status: a mismatch (DM to a non-connection, invite to someone already\n  connected/pending) returns error_code='channel_unavailable' with the safe\n  next action — pick a send_type that matches, do not retry blindly. Same\n  manual safety floor as `send`: suppressed/archived recipients rejected,\n  idempotent on retry, and the per-person ~20h cooldown applies to ALL\n  three types including connection requests (error_code='cooldown_active').\n  override_cooldown/override_reason work as in `send` (owner/operator only).\n\nBulk approve/reject hits the same endpoint the dashboard's bulk triage\nuses (max 50 per call — the target's own ceiling); `payload` may carry\nits extra fields but not message_ids — ids always travel via `id`.\nregenerate arrays loop with per-item status (max 100); edit takes a\nscalar id only. Partial failure is reported per item, never silent.\nPayloads are validated by the underlying route's own schema.\n\nProse gate: approve and edit re-lint the content server-side and can\nreturn error_code='prose_gate_blocked' with structured `failures[].fix`.\n`payload` accepts `reason` (corpus note), `override_reason` (taste\noverride — proceeds despite block failures, logged to the corpus;\nhonored only for owner/operator roles on the tenant, otherwise ignored\nand the gate blocks normally), and `client_rules_version` (responses\nflag `rules_changed` when the server's rules differ). Pre-check drafts\ncheaply with check_prose.","operationId":"manage_messages","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WriterRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/facade/check_prose":{"post":{"tags":["facade"],"summary":"Check Prose","description":"Check a prose draft against the gate BEFORE submitting it — free to iterate.\n\nSame engine and same structured failures as the blocking gate at\nmanage_messages / manage_engagements / manage_content: fix each\n`failures[].fix` until `outcome=passed`, then submit. Pass `item_id` +\n`item_type` ('engagement'|'message') and the server loads the item's real\ncontext (source post, dossier, channel, cross-prospect repetition window)\nfor exact parity; or pass `surface` + `content` (+ `channel`/`post_text`)\nto check free-standing text. `rules_only=true` returns the surface's\ncurrent rules + `rules_version` (compare to your embedded copy).\nDeterministic lint only — taste judgment is the calling harness's job.\nRead-only: nothing is written.","operationId":"check_prose","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckProseRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/facade/manage_outreach":{"post":{"tags":["facade"],"summary":"Manage Outreach","description":"Start and steer outreach plans — the consolidated outreach writer.\n\nActions:\n- start: enroll a person in outreach. id = PERSON UUID (scalar starts\n  one plan; an id ARRAY routes to the native bulk-start endpoint).\n  `payload` optional: {max_touches?, allowed_channels?,\n  start_immediately?} — applied to every started plan.\n- plan_override: record a user override on a plan (id = plan UUID;\n  `payload` = the override fields).\n- channels: update a plan's allowed channels (id = plan UUID; `payload`\n  = {allowed_channels, …}).\n- trigger_touch: queue the plan's next touch now (id = plan UUID; no\n  payload).\n- pause / resume: pause or resume a plan (id = plan UUID; `payload`\n  optional extras, e.g. {max_touches}).\n\nBulk: `start` arrays hit the native bulk endpoint; pause/resume/\nplan_override/channels/trigger_touch arrays loop with per-item status —\npartial failure is reported per item, never silent (max 100). Payloads\nare validated by the underlying route's own schema.","operationId":"manage_outreach","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WriterRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/facade/manage_campaign":{"post":{"tags":["facade"],"summary":"Manage Campaign","description":"Create and manage campaigns — the consolidated campaign writer.\n\nUse `search` with `type='campaigns'` to list campaigns. Use `fetch` with\n`type='campaign'` and `id=<campaign_uuid>` to read one campaign. Use this\nwriter for every campaign mutation and diagnosis.\n\nOutreach actions (default kind; id = campaign UUID):\n- create: create a campaign (no id; `payload` = the campaign fields);\n  bulk via `items`.\n- clone: duplicate a campaign (`payload` = {name} for the copy).\n- update: update campaign fields — tone, cadence, targeting (`payload`).\n- delete: permanently delete a campaign. DESTRUCTIVE.\n- members: add/remove people (`payload` = {action: add|remove,\n  person_ids: [...], confirm?}).\n- diagnose: run the campaign health diagnosis (read-back; no payload).\n- pause: campaign-level kill switch (no payload). Sets the campaign paused\n  AND stops every active plan under it in one call — no per-plan pause\n  loop. Returns {plans_paused}. `id` may be an array to pause several\n  campaigns.\n- resume: clear the pause and restart ONLY the plans this campaign paused\n  (reschedules them for immediate execution). Plans paused for a reply,\n  OOO, or rejected draft — or paused individually — are left untouched.\n  No payload; returns {plans_resumed}.\n\nAd actions (kind='ad'; id = ad-campaign UUID): approve, reject, pause,\nresume, edit_budget — `payload` carries the action's fields (e.g.\n{daily_budget_cents} for edit_budget, {rejection_reason} for reject).\nboost (no id — there's no campaign yet): run a paid LinkedIn campaign.\n`payload` carries {content_post_id (sponsor an EXISTING published post)\nXOR creative_id (a stored VIDEO creative → Direct Sponsored Content,\nMETRICS-ONLY — no engager bridge), vehicle?, objective?,\nbudget:{daily_budget_cents/total_budget_cents}, audience (see below),\nduration_days?, approval_mode?('draft'|'auto'), integration_id?}.\n\nBoost audience — exactly one of:\n- {facets}: the PRIMARY form. LinkedIn-native firmographic targeting: a\n  map of facet key (titles, seniorities, industries, locations,\n  staff_count_ranges) → list of LinkedIn entity URNs. 'locations' is\n  REQUIRED (LinkedIn rejects campaigns without location targeting).\n  Values must be entity URNs, never display names — resolve names FIRST\n  via `fetch type='ads' subtype='targeting_entities'\n  filters={facet, q}` (that resolve-then-boost flow is the happy path).\n- {matched_audience_id}: a PRE-EXISTING uploaded matched audience only.\n- {criteria}: partner-gated — rejected 400 with code\n  matched_audience_partner_gated (LinkedIn's audience-member API needs\n  partner access); the error's `fix` shows the facets form to use instead.\n\nVehicle/objective/format legality (enforced server-side BEFORE any spend;\nillegal combos 400 with the legal alternatives enumerated):\n| vehicle          | objectives                          | source            | destination CTA        |\n|------------------|-------------------------------------|-------------------|------------------------|\n| thought_leader   | BRAND_AWARENESS, ENGAGEMENT only    | member post only  | NOT allowed            |\n| page_sponsored   | all (BRAND_AWARENESS, ENGAGEMENT,   | org post or video | required for           |\n|                  | WEBSITE_VISIT, WEBSITE_CONVERSION,  | creative_id (DSC) | WEBSITE_VISIT /        |\n|                  | LEAD_GENERATION, VIDEO_VIEW)        |                   | WEBSITE_CONVERSION     |\nDestination = the post's external_link (set at draft/edit via\nmanage_content) or the video creative's cta_destination_url. Vehicle is\ninferred from the post's publish identity when omitted (member ⇒\nthought_leader, organization ⇒ page_sponsored). Objectives use LinkedIn's\nenum verbatim — do not lowercase.\n\nstore_creative (no id — there's no campaign yet): store a harness-generated\nad creative as a DRAFT (zero spend; approval is separate). IMAGE `payload`:\n{image_base64 (raw base64 PNG/JPEG, no data: URL), generation_prompt,\ncampaign_id? (owned draft warming campaign to attach), generation_provenance?\n({model, tool, generated_at, notes} allow-list, string values)}. VIDEO\n`payload` (mp4, ≤200MB, 3s–30min): {media_url (public https — fetched +\nvalidated in the background)} OR {filename, size_bytes, content_type\n('video/mp4') → returns a presigned upload_url; PUT the file there (curl),\nthen call store_creative again with {creative_id} to validate/finalize}.\nOptional {thumbnail_base64}. Video stores are ASYNC — poll\n`fetch type='ads' subtype='creative' id=<creative_id>` until upload_status\nleaves 'uploading' ('failed' + error_message starting with a probe code\nmeans re-export/re-store; otherwise retriable).\n\nIdentity setup (kind='ad', no id — VRU-659):\n- set_page: `payload` = {organization_urn? (urn:li:organization:<digits>),\n  integration_id?}. Omit organization_urn to DISCOVER the candidate Pages\n  from the connected LinkedIn accounts, then call again with one. Required\n  before page_sponsored campaigns and video ads.\n- authorize_author: `payload` = {author_user_id, auto_approve?,\n  integration_id?} — records the operator ATTESTATION that this team\n  member consented to Thought Leader Ad sponsorship (required before a\n  thought_leader boost; LinkedIn still enforces its own permission).\n  Response lists the currently authorized authors.\n- revoke_author: `payload` = {author_user_id, integration_id?}.\n\nBulk: `id` arrays loop with per-item status (partial failure is reported\nper item, never silent; max 100). Payloads are validated by the\nunderlying route's own schema.","operationId":"manage_campaign","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignWriterRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/facade/manage_engagements":{"post":{"tags":["facade"],"summary":"Manage Engagements","description":"Triage the LinkedIn engagement queue — the consolidated engagement writer.\n\nActions (id = engagement UUID; arrays route natively — the underlying\nendpoint is bulk-shaped):\n- approve: approve engagement(s) for sending (`payload` optional extras,\n  e.g. {polish_provenance}).\n- edit: replace an engagement's content (`payload` = {content,\n  content_mode?, reaction_type?}).\n- skip: discard engagement(s) — the queue's reject path (no new\n  engagement is generated).\n- draft_comment: queue a comment on a LinkedIn post for AUTHORING\n  (creates a needs_draft entry — write the comment via edit, then\n  approve; no id; `payload` = {post_text, post_id, author_name?,\n  person_id?, tone?, …}).\n\nBulk: approve/edit/skip take id arrays natively (max 100) with the\nendpoint's own per-engagement results. Payloads are validated by the\nunderlying route's schema.\n\nProse gate: approve and edit re-lint the content server-side and can\nreturn error_code='prose_gate_blocked' with structured `failures[].fix`.\n`payload` accepts `reason` (corpus note), `override_reason` (taste\noverride — proceeds despite block failures, logged to the corpus;\nhonored only for owner/operator roles on the tenant, otherwise ignored\nand the gate blocks normally), and `client_rules_version` (responses\nflag `rules_changed` when the server's rules differ). Pre-check drafts\ncheaply with check_prose.","operationId":"manage_engagements","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WriterRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/facade/manage_content":{"post":{"tags":["facade"],"summary":"Manage Content","description":"Draft, schedule and publish LinkedIn content — the consolidated content writer.\n\nActions (id = post UUID unless noted):\n- draft: create a post draft (no id; `payload` = {content, topic_tags?,\n  scheduled_at?, author_user_id?, author_identity?, external_link?});\n  bulk via `items`.\n- edit: update a draft's content/tags/identity/link (`payload` = the\n  fields to change: content?, topic_tags?, author_identity?,\n  external_link? — empty-string external_link clears it).\n- schedule: set the publish time (`payload` = {scheduled_at}).\n- publish: publish the post now (`payload` optional: {creative_id?} to\n  attach a stored ads VIDEO creative).\n- delete: remove a draft from the calendar (drafts only — published\n  posts are not deletable here).\n- draft_post: never generates prose — backend generation is removed\n  (VRU-676, permanently). Always returns author-in-harness guidance:\n  write the post yourself and submit via draft, then schedule/publish\n  (no id; `payload` = {topic?, content_type?, author_user_id?}).\n\nIdentities (VRU-659): `author_identity` is 'member' (the author's\npersonal profile — default) or 'organization' (the Company Page).\nOrganization publish requires a resolvable Company Page on the author's\nLinkedIn account: exactly one Page on the account, or a selected Page\n(manage_campaign kind='ad' action='set_page'); the member account still\nauthenticates and must hold admin rights on the Page.\n\n`external_link` (http/https, <=2000 chars) renders as the LinkedIn\npreview card; UTM params are stamped when the link is SAVED (missing\nutm_* added, existing ones never clobbered), so the approved draft is\nexactly what publishes.\n\nVideo attach: publish `payload.creative_id` names a stored ads video\ncreative (ad_creatives, format='video', uploaded asset required). The\nmedia transfer to LinkedIn runs INLINE in the publish call — expect tens\nof seconds for a large video.\n\nBulk: edit/schedule/publish/delete take id arrays (server-side loop,\nper-item status — partial failure reported per item, never silent; max\n100). Payloads are validated by the underlying route's schema.\n\nProse gate: draft/edit lint the content and schedule/publish re-lint it\nserver-side; blocked calls return error_code='prose_gate_blocked' with\nstructured `failures[].fix`. `payload` accepts `override_reason` (taste\noverride — proceeds despite block failures, logged to the corpus;\nhonored only for owner/operator roles on the tenant, otherwise ignored\nand the gate blocks normally) and `client_rules_version` (responses\nflag `rules_changed` when the server's rules differ). Pre-check drafts\ncheaply with check_prose.","operationId":"manage_content","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WriterRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/facade/manage_kb":{"post":{"tags":["facade"],"summary":"Manage Kb","description":"Maintain the knowledge base — the consolidated KB writer.\n\nActions (id = document UUID):\n- upsert: create or update a document (no id — the payload's `name` is\n  the upsert key; `payload` = {name, content, doc_type?, summary?,\n  metadata?}); bulk via `items`.\n- delete: permanently delete a document. DESTRUCTIVE.\n- reindex: rebuild a document's search index (no payload).\n\nBulk: delete/reindex take id arrays (server-side loop, per-item status —\npartial failure reported per item, never silent; max 100). Payloads are\nvalidated by the underlying route's schema.","operationId":"manage_kb","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WriterRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/facade/manage_account":{"post":{"tags":["facade"],"summary":"Manage Account","description":"Maintain account state and impact history — the consolidated account writer.\n\nActions:\n- state: update an account's lifecycle state (id = the account's\n  company_id; `payload` = {account_stage?, health_score?, arr_current?,\n  renewal_at?, notes?, …}).\n- record_impact: log a value-delivered impact event on an account\n  (id = company_id; `payload` = {practice, event_type, person_id?,\n  value_delivered_numeric?, summary?, …}).\n- delete_impact: permanently remove an impact event (id = the impact\n  EVENT/activity id, not the account). DESTRUCTIVE.\n\nBulk: id arrays loop with per-item status (partial failure reported per\nitem, never silent; max 100). Payloads are validated by the underlying\nroute's schema.","operationId":"manage_account","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WriterRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/facade/manage_settings":{"post":{"tags":["facade"],"summary":"Manage Settings","description":"Update company configuration — the consolidated settings writer.\n\nActions (no ids — settings are singletons per company):\n- profile: update the company profile / ICP (`payload` = the profile\n  fields; this steers research match scoring).\n- auto_fill: AI-fill the company profile from the website (`payload`\n  optional: {website_url?}; returns a job to poll via\n  fetch type='job').\n- automation: update outreach automation settings (`payload` = the\n  automation fields).\n- marketing: update LinkedIn marketing settings (`payload` = the\n  marketing fields).\n- cta_link: manage call-to-action links (`payload` = {action:\n  create|update|delete, cta_link_id?, name?, url?, cta_type?,\n  is_active?}).\n- hubspot_sync_settings: set the custom HubSpot contact properties to\n  mirror into company_people.custom_fields (`payload` =\n  {extra_contact_properties: [...]}); CHANGING the list auto-enqueues a\n  full backfill so existing contacts pick up the new attributes.\n- hubspot_backfill: kick a full HubSpot re-pull for this company (owners\n  → companies → pipelines → contacts → deals → lists → engagements). No\n  payload. Heavy (hours at 250k+ contacts). The recovery path when\n  contacts were imported before their custom properties were configured.\n  Operators target a client with `for_company`.\n\nPayloads are validated by the underlying route's own schema, so a bad\nfield returns that route's precise 422.","operationId":"manage_settings","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WriterRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/facade/research":{"post":{"tags":["facade"],"summary":"Research","description":"Run prospect/company research and save the results — the consolidated research tool.\n\nLookup actions (external providers — results are returned, not stored,\nunless noted):\n- find_linkedin: locate a person's LinkedIn URL (id = person UUID;\n  `payload` optional {name_override?, company_override?, auto_save?} —\n  auto_save stores a high-confidence match).\n- enrich_company: scrape a company website (`payload` = {domain,\n  page_type?}).\n- find_email: find a work email (`payload` = {first_name, last_name,\n  domain}).\n- linkedin_fetch: pull a LinkedIn profile (`payload` = {linkedin_url,\n  include_company?, include_posts?, posts_limit?}).\n- careers_parse: parse a careers page for hiring signals (`payload` =\n  {domain}).\n\nSave actions (write research rows):\n- save_person: save person research (`payload` = the person-research\n  fields).\n- save_company: save company research (`payload` = the company-research\n  fields).\n\nBatch research: every action accepts `items` (one payload per lookup,\nmax 100, per-item results — partial failure reported per item, never\nsilent). find_linkedin also takes person-id arrays. Payloads are\nvalidated by the underlying route's own schema.","operationId":"research","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WriterRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/facade/import_prospects":{"post":{"tags":["facade"],"summary":"Import Prospects","description":"Source and import prospects — the consolidated import tool.\n\nSales Navigator actions (`payload` = the search/import fields):\n- sales_nav_search: search LinkedIn people ({keywords, mode?,\n  title?, company_headcount_min/max?, list_id?, saved_search_id?,\n  limit?, cursor?}). `mode` must be `basic` or `sales_nav`; use\n  `basic` for exact-name/profile resolution fallback when Sales Nav is\n  unavailable.\n- sales_nav_preview: preview a Sales Nav search against your pipeline\n  ({search?, count?, campaign?}).\n- sales_nav_import: import profiles ({search?, count?, profiles_json?,\n  auto_enroll?, campaign?}).\n- sales_nav_dismiss: dismiss profiles from future imports\n  ({linkedin_urls} — comma-separated).\n- sales_nav_searches: manage saved searches ({action: list|create|\n  update|delete, name?, keywords?, filters_json?, frequency?,\n  search_id?}).\n\nCSV actions:\n- csv_preview: parse + preview a CSV payload ({csv_content, …}) before\n  importing.\n- csv_start: start an import job ({file_content, column_mapping,\n  campaign_id?, skip_research?}). Returns a job id — poll it with\n  fetch type='csv_import'.\n\nPayloads are validated by the underlying route's own schema, so a bad\nfield returns that route's precise 422.","operationId":"import_prospects","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WriterRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/facade/skill":{"post":{"tags":["facade"],"summary":"Skill","description":"Invoke or publish Vruum skills — the consolidated skill tool.\n\nActions:\n- invoke: fetch a skill's executable body for inline execution\n  (id = skill UUID or name; no payload). Follow the returned\n  instructions in your current context.\n- publish: publish or update a skill (no id; `payload` = {body,\n  scope?, metadata?, supporting_files?} — `body` is the full skill\n  markdown with frontmatter).\n\nTo browse skills use search type='skills'; to read one without\nexecuting it use fetch type='skill'.","operationId":"skill","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WriterRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/facade/get_operator_overview":{"post":{"tags":["facade"],"summary":"Get Operator Overview","description":"Cross-client operator overview — the consolidated operator read.\n\nViews: dashboard (all-clients summary), queue (pending items across\nclients — filters: {company_id?, channel?, category?, limit?, offset?}),\npipeline, health, campaign_overview, weekly_digest, companies (the\nclient list), pipeline_sources (configured sourcing mappings —\nfilters: {for_company?}).\n\nOperator sessions only. Results are returned verbatim from the\nunderlying operator endpoint under {view, results}.","operationId":"get_operator_overview","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OperatorOverviewRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/facade/manage_clients":{"post":{"tags":["facade"],"summary":"Manage Clients","description":"Operator client management — the consolidated operator writer.\n\nActions (operator sessions only; `payload` = the underlying request\nfields):\n- provision: provision a new client tenant ({company fields, …}).\n  DESTRUCTIVE-class (creates accounts and seats).\n- queue_action: act on queued items across clients ({message_ids,\n  action}).\n- shared_accounts: manage shared sending accounts ({the\n  shared-accounts fields}).\n- pipeline_sources: manage client sourcing mappings ({action,\n  mapping_id?, campaign_id?, search_url?, daily_target?, is_active?,\n  …}). The read side lives on get_operator_overview\n  view='pipeline_sources'.\n\nPayloads are validated by the underlying route's own schema. The\n`for_company` context-switch works exactly as on the granular tools.","operationId":"manage_clients","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WriterRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/facade/manage_seo_geo":{"post":{"tags":["facade"],"summary":"Manage Seo Geo","description":"Search visibility (SEO + GEO) — monitor share-of-voice in AI answers and turn gaps into actions.\n\nTracks where the tenant is absent from AI answers (ChatGPT, Perplexity,\nGoogle AI Overviews, Copilot) via a rented monitoring provider, and\nclassifies each visibility gap into a prioritized, ready-to-act item.\nTreat SEO and GEO as one discipline: engines cite pages the web indexes\nrank, so gap remediation is content + SEO work (publish indexable,\nanswer-shaped pages on the tenant's domain) — each returned gap action\ncarries that playbook in `recommended_action`.\n\nActions (tenant-level — no id; `payload` optional {prompt_set_id} to\ntarget a specific prompt set, else the tenant's active one):\n- run: pull fresh per-prompt mentions + citations from the provider and\n  store them (idempotent per provider run; COSTS provider spend).\n- read: return the latest daily share-of-voice rollup per engine plus the\n  open gap actions. Cheap — no provider call.\n- gap_analysis: classify the latest complete run into gap actions\n  (absent / outranked / no_citation → seed_third_party_source /\n  create_owned_asset / monitor_only) and return the open action list\n  for operator triage.\n\nReturns `{\"status\": \"not_configured\"}` when the tenant has no active GEO\nprompt set — monitoring is OFF by default and no provider call is made.","operationId":"manage_seo_geo","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WriterRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Vruum login token or API token."}},"schemas":{"ActivityActor":{"properties":{"type":{"type":"string","title":"Type"},"user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Id"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"agent":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent"},"source_system":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source System"},"sender_email_config_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sender Email Config Id"},"sender_channel_config_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sender Channel Config Id"},"sender_label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sender Label"}},"type":"object","required":["type"],"title":"ActivityActor","description":"Who/what performed an activity (VRU-617). Built from the person_activities\nactor columns. `type` is derived: agent > human > system. `name` /\n`sender_label` (display resolution) are populated in a follow-up — the\nstructured ids + agent are the machine-actionable surface the harness needs."},"BriefingResponse":{"properties":{"pending_approvals_count":{"type":"integer","title":"Pending Approvals Count","default":0},"messages_needing_authoring":{"type":"integer","title":"Messages Needing Authoring","default":0},"engagements_needing_authoring":{"type":"integer","title":"Engagements Needing Authoring","default":0},"oldest_needs_draft_age_days":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Oldest Needs Draft Age Days"},"top_pending_approvals":{"items":{"$ref":"#/components/schemas/PendingApprovalPreview"},"type":"array","title":"Top Pending Approvals","default":[]},"new_replies_count":{"type":"integer","title":"New Replies Count","default":0},"recent_replies":{"items":{"$ref":"#/components/schemas/ReplyPreview"},"type":"array","title":"Recent Replies","default":[]},"meetings_booked_this_week":{"type":"integer","title":"Meetings Booked This Week","default":0},"pipeline_summary":{"items":{"$ref":"#/components/schemas/PipelineStage"},"type":"array","title":"Pipeline Summary","default":[]},"stalled_deals":{"items":{"$ref":"#/components/schemas/StalledDeal"},"type":"array","title":"Stalled Deals","default":[]},"deal_alerts":{"items":{"$ref":"#/components/schemas/DealAlert"},"type":"array","title":"Deal Alerts","default":[]},"deal_alerts_count":{"type":"integer","title":"Deal Alerts Count","default":0},"active_outreach_plans":{"type":"integer","title":"Active Outreach Plans","default":0},"discovery_jobs_in_progress":{"type":"integer","title":"Discovery Jobs In Progress","default":0},"tasks_due_today_count":{"type":"integer","title":"Tasks Due Today Count","default":0},"top_tasks_due":{"items":{"$ref":"#/components/schemas/TaskDue"},"type":"array","title":"Top Tasks Due","default":[]},"new_warm_paths_count":{"type":"integer","title":"New Warm Paths Count","default":0},"new_warm_paths":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"New Warm Paths","default":[]},"latest_logged_meeting_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Latest Logged Meeting At"},"top_recommendation":{"type":"string","title":"Top Recommendation","default":"all_clear"},"recommendation_detail":{"type":"string","title":"Recommendation Detail","default":"Nothing urgent — you're all caught up."},"sections_degraded":{"items":{"type":"string"},"type":"array","title":"Sections Degraded","default":[]},"suggested_actions":{"items":{"$ref":"#/components/schemas/SuggestedAction"},"type":"array","title":"Suggested Actions","default":[]}},"type":"object","title":"BriefingResponse"},"CampaignWriterRequest":{"properties":{"action":{"type":"string","title":"Action","description":"The operation to perform — see the tool description for the action vocabulary and each action's id/payload semantics."},"id":{"anyOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Id","description":"Target entity id, or an ARRAY of ids to apply the same action+payload to each (max 100; see the tool description for which entity the id names per action)."},"payload":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Payload","description":"Action-specific fields, validated by the underlying route's own schema (a bad field 422s with that route's precise errors). See the tool description per action."},"items":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Items","description":"For create-like actions only: an array of per-item payloads processed in order with per-item results (max 100). Mutually exclusive with `id` and `payload`."},"kind":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Kind","description":"Campaign family: 'outreach' (default) for outreach campaigns, or 'ad' to manage ad campaigns (actions: approve, reject, pause, resume, edit_budget, boost, store_creative, set_page, authorize_author, revoke_author).","default":"outreach"}},"additionalProperties":false,"type":"object","required":["action"],"title":"CampaignWriterRequest","description":"Campaign writer request — adds the ``kind`` family selector."},"CheckProseRequest":{"properties":{"surface":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Surface","description":"Prose surface: engagement_comment | cold_email | linkedin_message | reply_draft | content_post. Required unless item_id resolves it."},"content":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content","description":"The draft text to check (required unless rules_only)."},"item_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Item Id","description":"Optional queue-item UUID. With item_type, the server loads the real context (source post, dossier, channel, repetition window) so the check is exact parity with the submission gate."},"item_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Item Type","description":"'engagement' or 'message' (with item_id)."},"channel":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel","description":"Channel for per-channel caps (linkedin_connection/linkedin_message/linkedin_inmail/email) when no item_id."},"post_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Post Text","description":"Source post text (engagement overlap check) when no item_id."},"rules_only":{"type":"boolean","title":"Rules Only","description":"Return the surface's current prose rules + rules_version instead of checking a draft (for embedded-rules drift detection).","default":false},"client_rules_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Rules Version","description":"The rules_version the caller authored against; response sets rules_changed when it differs."}},"type":"object","title":"CheckProseRequest"},"ContentReviewItem":{"properties":{"post_id":{"type":"string","title":"Post Id"},"content":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content"},"content_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content Type"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"topic_tags":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Topic Tags"},"scheduled_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scheduled At"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"past_performance":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Past Performance"},"calendar_neighbors":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Calendar Neighbors","default":[]},"content_tone_instructions":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content Tone Instructions"},"author_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Author User Id"}},"type":"object","required":["post_id"],"title":"ContentReviewItem"},"ContentReviewResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ContentReviewItem"},"type":"array","title":"Items","default":[]},"total_pending":{"type":"integer","title":"Total Pending","default":0},"offset":{"type":"integer","title":"Offset","default":0},"limit":{"type":"integer","title":"Limit","default":10},"calendar_summary":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Calendar Summary"}},"type":"object","title":"ContentReviewResponse"},"DealAlert":{"properties":{"deal_id":{"type":"string","title":"Deal Id"},"deal_name":{"type":"string","title":"Deal Name"},"alert_type":{"type":"string","title":"Alert Type"},"severity":{"type":"string","title":"Severity"},"detail":{"type":"string","title":"Detail"},"days":{"type":"integer","title":"Days"},"person_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Person Name"}},"type":"object","required":["deal_id","deal_name","alert_type","severity","detail","days"],"title":"DealAlert"},"EngagementReviewItem":{"properties":{"engagement_id":{"type":"string","title":"Engagement Id"},"engagement_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Engagement Type"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"content":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content"},"reaction_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reaction Type"},"icp_match_reasoning":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Icp Match Reasoning"},"priority_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Priority Score"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"target_post_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Post Text"},"target_post_author":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Post Author"},"person_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Person Id"},"person_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Person Name"},"person_title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Person Title"},"person_company":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Person Company"},"match_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Match Score"},"stage":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stage"},"campaign_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Name"},"campaign_tone_instructions":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Tone Instructions"},"campaign_selling_strategy":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Selling Strategy"},"match_summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Match Summary"},"alignment_points":{"anyOf":[{"items":{},"type":"array"},{"type":"null"}],"title":"Alignment Points"},"recommended_approach":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Recommended Approach"},"plan_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Plan Status"},"plan_current_touch":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Plan Current Touch"},"warming_engagements_sent":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Warming Engagements Sent"},"nurture_engagements_sent":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Nurture Engagements Sent"},"warming_graduation_threshold":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Warming Graduation Threshold"},"nurture_graduation_threshold":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Nurture Graduation Threshold"},"recent_engagement_history":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Recent Engagement History","default":[]},"company_summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Summary"},"budget_status":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Budget Status"},"bundle_siblings":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Bundle Siblings","default":[]},"engagement_group_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Engagement Group Id"},"schema_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Schema Version","default":"2026.07.04"},"rules_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rules Version"},"dossier":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Dossier"},"polish_provenance":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Polish Provenance"},"validator_failures":{"anyOf":[{"items":{},"type":"array"},{"type":"null"}],"title":"Validator Failures"},"pitch_phrases":{"anyOf":[{"items":{},"type":"array"},{"type":"null"}],"title":"Pitch Phrases"}},"type":"object","required":["engagement_id"],"title":"EngagementReviewItem"},"EngagementReviewResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/EngagementReviewItem"},"type":"array","title":"Items","default":[]},"total_pending":{"type":"integer","title":"Total Pending","default":0},"offset":{"type":"integer","title":"Offset","default":0},"limit":{"type":"integer","title":"Limit","default":10}},"type":"object","title":"EngagementReviewResponse"},"FacadeBatchPersonQuery":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name","description":"Person full name to look up in the existing pipeline."},"company":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company","description":"Company name to look up alongside the person name."},"linkedin_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Linkedin Url","description":"LinkedIn profile URL — strongest dedup signal; exact-matched first when present."}},"additionalProperties":false,"type":"object","title":"FacadeBatchPersonQuery","description":"One candidate in a people batch lookup (maps to ``BatchSearchRequest.queries``)."},"FacadeCampaignsFilters":{"properties":{"fields":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fields","description":"Projection: omit for the default compact list (the prose-heavy ai_tone_instructions/ai_selling_strategy/touch_sequence come back null); 'minimal' for id/name/slug only (dropdowns/selection); 'full' to include the AI-strategy prose for EVERY campaign (10KB+ each — a multi-campaign tenant can exceed client output limits and get silently truncated; prefer fetch type='campaign' for one campaign's full config)."}},"additionalProperties":false,"type":"object","title":"FacadeCampaignsFilters","description":"Filters for ``type='campaigns'`` (routes to ``get_campaigns``)."},"FacadeCompaniesFilters":{"properties":{"domain":{"type":"string","title":"Domain","description":"Company domain to search (e.g. 'acme.com'). Required."},"seniority":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Seniority","description":"Seniority filter: executive (C-level/VP), senior (directors/managers), or junior."}},"additionalProperties":false,"type":"object","required":["domain"],"title":"FacadeCompaniesFilters","description":"Filters for ``type='companies'`` — EXTERNAL Hunter.io domain search via ``find_people_at_company``."},"FacadeContentFilters":{"properties":{"days":{"type":"integer","minimum":1.0,"title":"Days","description":"Number of days of calendar history to show (default 14 — same as the underlying endpoint).","default":14}},"additionalProperties":false,"type":"object","title":"FacadeContentFilters","description":"Filters for ``type='content'`` (routes to ``get_content_calendar``)."},"FacadeCtaLinksFilters":{"properties":{},"additionalProperties":false,"type":"object","title":"FacadeCtaLinksFilters","description":"Filters for ``type='cta_links'`` (routes to ``get_cta_links``).\n\nThe underlying endpoint takes no filters and no pagination — this model is\nintentionally empty (pagination on ``cta_links`` is rejected at the\nrequest level, never silently ignored)."},"FacadeDealsFilters":{"properties":{"stage":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stage","description":"Deal stage filter. Matches the deal's pipeline-stage label LITERALLY (exact, case-sensitive string — not a closed enum). Native pipelines use met, qualified, proposal, negotiation, verbal_yes, closed_won, closed_lost; HubSpot-imported pipelines carry their own custom labels (e.g. 'Hot Leads', 'Prospecting', 'Trial Signup', 'Closed lost'). Pass the literal label exactly as stored. Discover the labels a tenant's pipeline actually uses via inspect_pipeline (groups deals by stage) or by listing deals (with no stage filter) and reading each deal's 'stage'."},"outcome":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Outcome","description":"Deal outcome filter: won, lost, stalled."},"query":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Query","description":"Free-text search for a specific deal — case-insensitive, every whitespace-separated word must match. Spans the deal name, the associated company name, and the primary contact's name/email/title. Combine with stage/outcome to scope further."}},"additionalProperties":false,"type":"object","title":"FacadeDealsFilters","description":"Filters for ``type='deals'`` (routes to ``get_deals``)."},"FacadeEngagementsFilters":{"properties":{"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","description":"Engagement status: draft, approved, sent, failed, skipped."},"sort_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sort By","description":"Sort: priority (default), created_at, or status.","default":"priority"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source","description":"Engagement source: warming, nurture, or marketing."},"campaign":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign","description":"Campaign NAME or UUID (resolved via the engagement queue's campaign-ref resolver — distinct from the messages campaign_id resolver)."}},"additionalProperties":false,"type":"object","title":"FacadeEngagementsFilters","description":"Filters for ``type='engagements'`` (routes to ``get_engagement_queue``)."},"FacadeFetchRequest":{"properties":{"type":{"type":"string","title":"Type","description":"Entity type to fetch. One of: campaign, csv_import, plan, conversation, person_research, company_research, account_state, insights, scoreboard, settings, billing, publish_readiness, research_playbook, job, skill, deal, deal_alerts, post_analytics, seller_signals, stats, marketing, ads."},"subtype":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subtype","description":"Variant selector — required for insights (quality | pattern_tags | prompts | benchmarks | improve | reply_diagnosis | mcp_patterns | mcp_pattern_detail), scoreboard (impact | bowtie) and settings (profile | automation | marketing | channel_status | booking_link). Other types take no subtype."},"id":{"anyOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Id","description":"Entity id (see the per-type semantics in the tool description). An ARRAY of ids is accepted for campaign, plan, person_research and company_research (max 100). For job polling, pass the scalar job UUID returned by async tools; a one-element array is accepted and normalized for agent ergonomics."},"filters":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Filters","description":"Extra query filters for the types that take them: stats.outreach {start_date?, end_date?}; stats.prose {days? (default 30, 1-90)}; marketing.activity {days?, limit?}; ads.attribution {window_days?, outreach_campaign_id?, by? (creative|audience)}; ads.warming_review {limit?, offset?, outreach_campaign?, segment?}; ads.targeting_entities {facet (titles|seniorities|industries|locations|staff_count_ranges), q (min 2 chars), integration_id?}; scoreboard {window_days?}; seller_signals {draft_brief?, include_posts?, include_notes?, include_knowledge?}. Unknown keys are rejected with 422; other types take no filters."}},"additionalProperties":false,"type":"object","required":["type"],"title":"FacadeFetchRequest","description":"Body for the polymorphic ``fetch`` facade tool."},"FacadeKbFilters":{"properties":{"query":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Query","description":"Fuzzy keyword search. Matches authored docs (name/summary/keywords, typo-tolerant) AND synced connector documents (Google Drive / Notion) by title + body — e.g. meeting transcripts."},"doc_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Doc Type","description":"Filter by type: positioning, case_study, objection_handling, battlecard, process, other (authored docs), or 'connector' for synced Drive/Notion documents only. Omit to search both."},"document_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Document Id","description":"Fetch full content of one document (ignores query/doc_type). Accepts an authored-doc UUID or a connector id of the form '<connection_uuid>:<source_record_id>' (as returned in search results)."},"include_content":{"type":"boolean","title":"Include Content","description":"Include full document text in search results (default false for a compact catalog). Connector docs return snippets in search mode regardless; read one in full via document_id.","default":false}},"additionalProperties":false,"type":"object","title":"FacadeKbFilters","description":"Filters for ``type='kb'`` (routes to ``search_knowledge_base``)."},"FacadeMessagesFilters":{"properties":{"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","description":"Message status: needs_draft (unauthored — author via manage_messages edit), draft, approved, rejected, sent (expands to post-send statuses), or all. Omit for default (needs_draft + draft + approved)."},"channel":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel","description":"Channel filter (email, linkedin_connection, linkedin_message, …)."},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category","description":"Message category: initial, followup, reply_response."},"has_reply":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Has Reply","description":"Only messages that have (true) / have not (false) received a reply."},"warm_only":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Warm Only","description":"true = only WARM follow-ups: linkedin_message rows whose plan has connection_status='connected' (the person ACCEPTED the connection request — highest-EV rows in the queue). Forces channel=linkedin_message. warm_only=true + status='needs_draft' + sort_by='expiring' pulls the warm authoring lane in one call, nearest sweep deadline first."},"person_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Person Id","description":"Filter by a specific person ID."},"search":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Search","description":"Search across subject, person email, person name."},"sort_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sort By","description":"Sort: recent (default), expiring (created_at asc — nearest sweep deadline first, pair with status='needs_draft'), person_name, subject, priority, or match_score.","default":"recent"},"sequence_number_min":{"anyOf":[{"type":"integer","minimum":1.0},{"type":"null"}],"title":"Sequence Number Min","description":"Min sequence number, inclusive (T1=1)."},"sequence_number_max":{"anyOf":[{"type":"integer","minimum":1.0},{"type":"null"}],"title":"Sequence Number Max","description":"Max sequence number, inclusive (T1=1)."},"score_min":{"anyOf":[{"type":"integer","maximum":100.0,"minimum":0.0},{"type":"null"}],"title":"Score Min","description":"Minimum match score (0-100)."},"score_max":{"anyOf":[{"type":"integer","maximum":100.0,"minimum":0.0},{"type":"null"}],"title":"Score Max","description":"Maximum match score (0-100)."},"campaign_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Id","description":"Campaign UUID, or '__none__' for people with no campaign assigned (resolved via the queue's campaign_id resolver)."},"fields":{"anyOf":[{"type":"string","const":"compact"},{"type":"null"}],"title":"Fields","description":"Set to 'compact' for a lightweight projection WITHOUT message body — keeps message_id, person_name, category, sequence_number, channel, status, match_score, touches_completed, campaign_id, first_content_touch, channel_rewrite_reason, expires_at, connection_status. first_content_touch=true = the person has never received a content-bearing message (connection requests don't count): treat the item as an OPENER regardless of sequence_number; null = row predates the conversation_state brief (fall back to the sequence_number heuristic). expires_at (needs_draft rows only) = when the nightly sweep auto-rejects the row if still unauthored — author oldest-first to beat it. connection_status = the linked plan's LinkedIn connection state: a linkedin_message row with connection_status='connected' is a WARM follow-up (they accepted the invite) — dispatch these first. Cheap on tokens for triage. An unrecognized value 422s."},"view":{"anyOf":[{"type":"string","const":"breakdown"},{"type":"null"}],"title":"View","description":"Set to 'breakdown' for a queue-composition summary: grouped counts (by campaign × category × sequence × channel × status, plus per-dimension marginals) over the full matching set, alongside a compact items page. Implies fields='compact'. An unrecognized value 422s."}},"additionalProperties":false,"type":"object","title":"FacadeMessagesFilters","description":"Filters for ``type='messages'`` (routes to ``get_message_queue``)."},"FacadePeopleFilters":{"properties":{"stage":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stage","description":"Pipeline stage: new, warming, enrolled, contacted, replied, met, opportunity, closed_won, closed_lost."},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","description":"Legacy filter — 'meeting_booked' (stage=met) or 'all' (default)."},"search":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Search","description":"Free-text search across email, first/last name, title, company name."},"sort_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sort By","description":"Sort by recent, score, name, or company (default: score — same as the underlying endpoint).","default":"score"},"enrollment":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Enrollment","description":"Outreach enrollment filter: all, enrolled, paused, not_enrolled."},"relationship_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relationship Type","description":"Relationship filter: outbound_prospect, inbound_buyer, inbound_seller, networking, or all."},"campaign":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign","description":"Outreach campaign UUID, exact name, slug, or 'unassigned' for people with no campaign."},"score_min":{"anyOf":[{"type":"integer","maximum":100.0,"minimum":0.0},{"type":"null"}],"title":"Score Min","description":"Minimum match score (0-100, inclusive)."},"score_max":{"anyOf":[{"type":"integer","maximum":100.0,"minimum":0.0},{"type":"null"}],"title":"Score Max","description":"Maximum match score (0-100, inclusive)."},"research_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Research Status","description":"'researched', 'shallow', 'deep', 'stub', or 'all'. When omitted, matches the underlying endpoint's default: 'researched' for browsing, but 'all' when `search` is set (a search must find stubs too)."},"custom":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Custom","description":"Custom-attribute filters from CSV-import custom columns; every key=value pair must match (e.g. {'sorted_company_size': 'small'})."},"list":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"List","description":"Only people in this list — people_lists UUID or exact name (CSV-import list mirroring). Unknown list returns an empty result."},"persona":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Persona","description":"Buying-role filter: influencer, decision_maker, economic_buyer, unknown, or 'unclassified' (no classification run yet)."}},"additionalProperties":false,"type":"object","title":"FacadePeopleFilters","description":"List-mode filters for ``type='people'`` (routes to ``get_user_people``).\n\nMutually exclusive with ``query`` — pass one or the other, never both."},"FacadeSearchRequest":{"properties":{"type":{"$ref":"#/components/schemas/SearchEntityType","description":"Entity type to search: people, companies, deals, campaigns, messages, engagements, content, kb, skills, or cta_links."},"query":{"anyOf":[{"type":"string"},{"items":{"$ref":"#/components/schemas/FacadeBatchPersonQuery"},"type":"array"},{"type":"null"}],"title":"Query","description":"Only for type='people'. A keyword string searches the existing pipeline (name/title/company); an array of {name?, company?, linkedin_url?} objects runs a batch dedup lookup (results in original order). Mutually exclusive with `filters`. For type='kb' or type='deals', use filters.query instead."},"filters":{"anyOf":[{"$ref":"#/components/schemas/FacadePeopleFilters"},{"$ref":"#/components/schemas/FacadeCompaniesFilters"},{"$ref":"#/components/schemas/FacadeDealsFilters"},{"$ref":"#/components/schemas/FacadeCampaignsFilters"},{"$ref":"#/components/schemas/FacadeMessagesFilters"},{"$ref":"#/components/schemas/FacadeEngagementsFilters"},{"$ref":"#/components/schemas/FacadeContentFilters"},{"$ref":"#/components/schemas/FacadeKbFilters"},{"$ref":"#/components/schemas/FacadeSkillsFilters"},{"$ref":"#/components/schemas/FacadeCtaLinksFilters"},{"type":"null"}],"title":"Filters","description":"Type-specific filters — the object shape is determined by `type` (e.g. {domain} for companies, {stage, outcome, query} for deals). Unknown keys are rejected with 422."},"limit":{"anyOf":[{"type":"integer","minimum":1.0},{"type":"null"}],"title":"Limit","description":"Page size. Supported for people, deals, messages (max 100), engagements (max 200); people keyword-query mode caps at 40. Rejected (422) for other types."},"offset":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Offset","description":"Pagination offset. Supported for people (list mode), deals, messages, engagements. Rejected (422) for other types and for people query mode."}},"additionalProperties":false,"type":"object","required":["type"],"title":"FacadeSearchRequest","description":"Request body for the polymorphic ``search`` facade tool."},"FacadeSkillsFilters":{"properties":{"scope":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scope","description":"Skill scope filter: private, company, or operator. Omit for every scope the caller can see."}},"additionalProperties":false,"type":"object","title":"FacadeSkillsFilters","description":"Filters for ``type='skills'`` (routes to the MCP ``list_skills`` route)."},"FindWarmPathRequest":{"properties":{"target":{"type":"string","title":"Target","description":"The person or company you want a warm route to. Accepts: a person UUID, a company UUID, a LinkedIn profile URL, an email address, or free text ('Jane Chen at Acme'). Resolution order for bare UUIDs: person first, then company."},"include_draft":{"type":"boolean","title":"Include Draft","description":"Also generate an intro-ask draft for the top path (an extra LLM call). Drafts are NEVER sent automatically.","default":false},"limit":{"type":"integer","maximum":20.0,"minimum":1.0,"title":"Limit","description":"Max paths to return.","default":5}},"type":"object","required":["target"],"title":"FindWarmPathRequest"},"FindWarmPathResponse":{"properties":{"status":{"type":"string","title":"Status","description":"found | no_match | ambiguous | not_found | sync_disabled | sync_pending"},"target":{"additionalProperties":true,"type":"object","title":"Target"},"paths":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Paths"},"candidates":{"items":{"$ref":"#/components/schemas/WarmPathCandidate"},"type":"array","title":"Candidates"},"sources":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Sources"},"diagnostics":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Diagnostics"},"intro_draft":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Intro Draft"},"draft_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Draft Note"},"next_step":{"type":"string","title":"Next Step","default":""}},"type":"object","required":["status"],"title":"FindWarmPathResponse"},"GetTasksResponse":{"properties":{"tasks":{"items":{"$ref":"#/components/schemas/TaskSummary"},"type":"array","title":"Tasks"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["tasks","total"],"title":"GetTasksResponse"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"ManageTasksRequest":{"properties":{"action":{"type":"string","enum":["create","update","complete","cancel","bulk_complete","delete"],"title":"Action"},"task_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Task Id","description":"Required for update/complete/cancel/delete."},"task_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Task Ids","description":"Required for bulk_complete."},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"person_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Person Id"},"deal_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Deal Id"},"company_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Id"},"assigned_to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Assigned To"},"due_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Due At"},"status":{"anyOf":[{"type":"string","enum":["open","in_progress","completed","cancelled"]},{"type":"null"}],"title":"Status"},"priority":{"anyOf":[{"type":"string","enum":["low","medium","high","urgent"]},{"type":"null"}],"title":"Priority"},"external_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Id"}},"type":"object","required":["action"],"title":"ManageTasksRequest"},"ManageTasksResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"task":{"anyOf":[{"$ref":"#/components/schemas/TaskSummary"},{"type":"null"}]},"tasks":{"anyOf":[{"items":{"$ref":"#/components/schemas/TaskSummary"},"type":"array"},{"type":"null"}],"title":"Tasks"},"message":{"type":"string","title":"Message"}},"type":"object","required":["success","message"],"title":"ManageTasksResponse"},"NextAction":{"properties":{"action_type":{"type":"string","title":"Action Type"},"priority":{"type":"integer","title":"Priority"},"title":{"type":"string","title":"Title"},"description":{"type":"string","title":"Description"},"person_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Person Id"},"person_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Person Name"},"message_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message Id"},"deal_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Deal Id"},"task_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Task Id"},"channel":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel"},"content_preview":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content Preview"},"campaign_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Name"}},"type":"object","required":["action_type","priority","title","description"],"title":"NextAction"},"NextActionsResponse":{"properties":{"actions":{"items":{"$ref":"#/components/schemas/NextAction"},"type":"array","title":"Actions","default":[]},"total":{"type":"integer","title":"Total","default":0}},"type":"object","title":"NextActionsResponse"},"OperatorOverviewRequest":{"properties":{"view":{"type":"string","title":"View","description":"Which operator view to read: dashboard, queue, pipeline, health, campaign_overview, weekly_digest, companies, or pipeline_sources."},"filters":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Filters","description":"View-specific query filters — `queue` takes {company_id?, channel?, category?, limit?, offset?}; `pipeline_sources` takes {for_company?}. Other views take none."}},"additionalProperties":false,"type":"object","required":["view"],"title":"OperatorOverviewRequest","description":"Request body for the operator overview read."},"OutreachReviewResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ReviewItem"},"type":"array","title":"Items","default":[]},"total_pending":{"type":"integer","title":"Total Pending","default":0},"offset":{"type":"integer","title":"Offset","default":0},"limit":{"type":"integer","title":"Limit","default":10},"approved_pending_send_count":{"type":"integer","title":"Approved Pending Send Count","default":0}},"type":"object","title":"OutreachReviewResponse"},"PendingApprovalPreview":{"properties":{"message_id":{"type":"string","title":"Message Id"},"person_name":{"type":"string","title":"Person Name"},"channel":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject"},"content_preview":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content Preview"}},"type":"object","required":["message_id","person_name"],"title":"PendingApprovalPreview"},"Person360Activity":{"properties":{"activity_type":{"type":"string","title":"Activity Type"},"description":{"type":"string","title":"Description"},"timestamp":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Timestamp"},"channel":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel"},"actor":{"anyOf":[{"$ref":"#/components/schemas/ActivityActor"},{"type":"null"}]}},"type":"object","required":["activity_type","description"],"title":"Person360Activity"},"Person360Deal":{"properties":{"deal_id":{"type":"string","title":"Deal Id"},"deal_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Deal Name"},"stage":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stage"},"estimated_value":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Estimated Value"},"close_probability":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Close Probability"},"next_step":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Step"},"next_step_due_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Step Due At"},"outcome":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Outcome"}},"type":"object","required":["deal_id"],"title":"Person360Deal"},"Person360MatchAnalysis":{"properties":{"match_summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Match Summary"},"alignment_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Alignment Score"},"overall_confidence":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Overall Confidence"},"recommended_approach":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Recommended Approach"},"why_now":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Why Now"},"potential_concerns":{"anyOf":[{"items":{},"type":"array"},{"type":"null"}],"title":"Potential Concerns"},"alignment_points":{"anyOf":[{"items":{},"type":"array"},{"type":"null"}],"title":"Alignment Points"}},"type":"object","title":"Person360MatchAnalysis"},"Person360Outreach":{"properties":{"plan_id":{"type":"string","title":"Plan Id"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"strategy":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Strategy"},"touches_completed":{"type":"integer","title":"Touches Completed","default":0},"max_touches":{"type":"integer","title":"Max Touches","default":0},"next_action_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Action At"},"next_action_channel":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Action Channel"},"conversation_messages":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Conversation Messages","default":[]},"warming_started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Warming Started At"},"warming_graduated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Warming Graduated At"},"warming_engagements_sent":{"type":"integer","title":"Warming Engagements Sent","default":0},"nurture_started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nurture Started At"},"nurture_engagements_sent":{"type":"integer","title":"Nurture Engagements Sent","default":0}},"type":"object","required":["plan_id"],"title":"Person360Outreach"},"Person360Person":{"properties":{"person_id":{"type":"string","title":"Person Id"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"first_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Name"},"last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Name"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"company":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company"},"linkedin_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Linkedin Url"},"phone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone"},"stage":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stage"},"match_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Match Score"},"conversation_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conversation Status"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"campaign_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Name"},"campaign_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Slug"},"current_positions":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Current Positions","default":[]}},"type":"object","required":["person_id"],"title":"Person360Person"},"Person360Research":{"properties":{"company_summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Summary"},"company_stage":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Stage"},"current_priorities":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Priorities"},"funding_data":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Funding Data"}},"type":"object","title":"Person360Research"},"Person360Response":{"properties":{"person":{"$ref":"#/components/schemas/Person360Person"},"match_analysis":{"anyOf":[{"$ref":"#/components/schemas/Person360MatchAnalysis"},{"type":"null"}]},"research":{"anyOf":[{"$ref":"#/components/schemas/Person360Research"},{"type":"null"}]},"activity":{"items":{"$ref":"#/components/schemas/Person360Activity"},"type":"array","title":"Activity","default":[]},"outreach":{"anyOf":[{"$ref":"#/components/schemas/Person360Outreach"},{"type":"null"}]},"deal":{"anyOf":[{"$ref":"#/components/schemas/Person360Deal"},{"type":"null"}]},"warm_path":{"anyOf":[{"$ref":"#/components/schemas/Person360WarmPath"},{"type":"null"}]}},"type":"object","required":["person"],"title":"Person360Response"},"Person360WarmPath":{"properties":{"status":{"type":"string","title":"Status"},"path":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Path"}},"type":"object","required":["status"],"title":"Person360WarmPath","description":"VRU-662: top warm path to this person from the user's own network.\n\n``status`` distinguishes \"no path\" from \"not computed\": found | no_match |\nsync_disabled | sync_pending. ``path`` carries the warm_path_service path\ndict (connector, score, score_breakdown, evidence) when status='found'."},"PipelineStage":{"properties":{"stage":{"type":"string","title":"Stage"},"count":{"type":"integer","title":"Count"},"total_value":{"type":"number","title":"Total Value","default":0.0}},"type":"object","required":["stage","count"],"title":"PipelineStage"},"ReplyPreview":{"properties":{"message_id":{"type":"string","title":"Message Id"},"person_name":{"type":"string","title":"Person Name"},"channel":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel"},"reply_sentiment":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reply Sentiment"},"replied_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Replied At"}},"type":"object","required":["message_id","person_name"],"title":"ReplyPreview"},"ReviewItem":{"properties":{"message_id":{"type":"string","title":"Message Id"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"channel":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Channel"},"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject"},"content":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"priority_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Priority Score"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"expires_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Expires At"},"person_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Person Id"},"person_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Person Name"},"person_title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Person Title"},"person_company":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Person Company"},"match_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Match Score"},"stage":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stage"},"campaign_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Name"},"campaign_tone_instructions":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Tone Instructions"},"campaign_selling_strategy":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Selling Strategy"},"campaign_touch_sequence":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Campaign Touch Sequence"},"match_summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Match Summary"},"alignment_points":{"anyOf":[{"items":{},"type":"array"},{"type":"null"}],"title":"Alignment Points"},"recommended_approach":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Recommended Approach"},"company_summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Summary"},"recent_linkedin_posts":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Recent Linkedin Posts"},"research_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Research Status"},"person_researched_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Person Researched At"},"company_researched_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Researched At"},"conversation_thread":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Conversation Thread","default":[]},"from_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"From Email"},"sending_mailbox":{"anyOf":[{"$ref":"#/components/schemas/SendingMailbox"},{"type":"null"}]},"outreach_plan_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Outreach Plan Id"},"plan_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Plan Status"},"plan_strategy":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Plan Strategy"},"touches_completed":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Touches Completed"},"max_touches":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Touches"},"plan_next_action_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Plan Next Action At"},"connection_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Connection Status"},"attachments":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Attachments","default":[]},"conversation_state":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Conversation State"}},"type":"object","required":["message_id"],"title":"ReviewItem"},"SearchEntityType":{"type":"string","enum":["people","companies","deals","campaigns","messages","engagements","content","kb","skills","cta_links","warm_paths"],"title":"SearchEntityType","description":"The 10 entity types the ``search`` facade can read."},"SendingMailbox":{"properties":{"sender_config_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sender Config Id"},"email_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Address"},"is_default":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Default"},"resolution":{"type":"string","title":"Resolution","default":"rotation_pending"}},"type":"object","title":"SendingMailbox","description":"VRU-653: the email mailbox a message sends / sent from.\n\n``sender_config_id`` is pinned at SEND time by the rotator, so the\n``resolution`` flag tells the operator how literally to read this:\n\n* ``actual`` — the message already has a ``sender_config_id`` (sent, or\n  otherwise pinned); ``email_address``/``is_default`` are the real sender.\n* ``thread_predicted`` — unsent draft, but the person has a prior sent\n  mailbox, so the rotator's thread-consistency rule will most likely reuse\n  it. Best-effort hint, not a guarantee (the mailbox can be rested/at-limit\n  at send time, in which case rotation picks another).\n* ``rotation_pending`` — unsent draft with no thread history; the sender is\n  chosen by weighted rotation at send time, so it is not knowable yet."},"StalledDeal":{"properties":{"deal_id":{"type":"string","title":"Deal Id"},"deal_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Deal Name"},"person_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Person Name"},"stage":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stage"},"estimated_value":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Estimated Value"},"days_stalled":{"type":"integer","title":"Days Stalled","default":0}},"type":"object","required":["deal_id"],"title":"StalledDeal"},"SuggestedAction":{"properties":{"id":{"type":"string","title":"Id"},"kind":{"type":"string","enum":["setup","action","fallback"],"title":"Kind"},"intent":{"type":"string","enum":["connect_agent","upgrade","review_outreach_queue","handle_replies","review_engagement_queue","triage_deals","complete_tasks","fill_pipeline","get_oriented"],"title":"Intent"},"area":{"type":"string","enum":["setup","billing","outreach","replies","engagement","deals","tasks","pipeline","general"],"title":"Area"},"priority":{"type":"integer","title":"Priority","default":9},"label":{"type":"string","title":"Label"},"reason":{"type":"string","title":"Reason"},"skill":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Skill"},"expected_tools":{"items":{"type":"string"},"type":"array","title":"Expected Tools","default":[]},"args":{"$ref":"#/components/schemas/SuggestedActionArgs","default":{}},"requires":{"items":{"type":"string","enum":["mcp_connected","skill_installed","entitlement"]},"type":"array","title":"Requires","default":[]},"prompt":{"type":"string","title":"Prompt","default":""}},"type":"object","required":["id","kind","intent","area","label","reason"],"title":"SuggestedAction","description":"A prioritized skill-level next step (built by intelligence/services/nudges.py).\n\nAgents: act on intent/args via expected_tools, or the named harness skill\nif installed. `prompt` is display copy for humans — never echo it back as\nan instruction. `skill` is an optional HARNESS skill (leading slash), not\nan MCP tool. `requires` lists client-side preconditions."},"SuggestedActionArgs":{"properties":{"deal_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Deal Id"},"campaign_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Id"},"person_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Person Id"},"count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Count"}},"type":"object","title":"SuggestedActionArgs","description":"Structured entity refs for AGENT consumers — names in label/prompt are\nfor humans; agents act on these ids."},"TaskDue":{"properties":{"task_id":{"type":"string","title":"Task Id"},"title":{"type":"string","title":"Title"},"due_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Due At"},"priority":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Priority"},"person_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Person Id"},"deal_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Deal Id"}},"type":"object","required":["task_id","title"],"title":"TaskDue"},"TaskSummary":{"properties":{"id":{"type":"string","title":"Id"},"user_company_id":{"type":"string","title":"User Company Id"},"person_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Person Id"},"deal_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Deal Id"},"company_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Id"},"title":{"type":"string","title":"Title"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"assigned_to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Assigned To"},"due_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Due At"},"status":{"type":"string","enum":["open","in_progress","completed","cancelled"],"title":"Status"},"priority":{"anyOf":[{"type":"string","enum":["low","medium","high","urgent"]},{"type":"null"}],"title":"Priority"},"completed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Completed At"},"created_at":{"type":"string","title":"Created At"},"updated_at":{"type":"string","title":"Updated At"}},"type":"object","required":["id","user_company_id","title","status","created_at","updated_at"],"title":"TaskSummary"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WarmPathCandidate":{"properties":{"kind":{"type":"string","title":"Kind"},"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"company":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company"}},"type":"object","required":["kind","id","name"],"title":"WarmPathCandidate"},"WriterRequest":{"properties":{"action":{"type":"string","title":"Action","description":"The operation to perform — see the tool description for the action vocabulary and each action's id/payload semantics."},"id":{"anyOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Id","description":"Target entity id, or an ARRAY of ids to apply the same action+payload to each (max 100; see the tool description for which entity the id names per action)."},"payload":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Payload","description":"Action-specific fields, validated by the underlying route's own schema (a bad field 422s with that route's precise errors). See the tool description per action."},"items":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Items","description":"For create-like actions only: an array of per-item payloads processed in order with per-item results (max 100). Mutually exclusive with `id` and `payload`."}},"additionalProperties":false,"type":"object","required":["action"],"title":"WriterRequest","description":"Uniform request body for every facade writer."}}}}