From 174726786c48147b8b1488add8462953fc3a4621 Mon Sep 17 00:00:00 2001 From: Waleed Date: Sat, 7 Nov 2026 11:24:06 -0800 Subject: [PATCH 01/29] docs(library): update best-ai-agents-for-executive-assistant-tasks (#9766) Co-authored-by: Sim Pi Agent --- .../index.mdx | 202 +++++++++++++----- 1 file changed, 144 insertions(+), 58 deletions(-) diff --git a/apps/sim/content/library/best-ai-agents-for-executive-assistant-tasks/index.mdx b/apps/sim/content/library/best-ai-agents-for-executive-assistant-tasks/index.mdx index f0d2c7f44cc..fac1c5a01c8 100644 --- a/apps/sim/content/library/best-ai-agents-for-executive-assistant-tasks/index.mdx +++ b/apps/sim/content/library/best-ai-agents-for-executive-assistant-tasks/index.mdx @@ -3,18 +3,18 @@ slug: best-ai-agents-for-executive-assistant-tasks title: 'Best AI Agents for Executive Assistant Tasks' description: 'Compare the best AI agents for executive assistant tasks across calendar management, email, research, approvals, integrations, human review, and deployment control.' seoTitle: 'Best AI Tools for Executive Assistants in 2026' -seoDescription: 'Sim, n8n, Zapier, Make and Lindy compared for executive assistant work: calendar, email, research, approvals and human review controls.' +seoDescription: 'Compare Sim, n8n, Zapier, Make, Copilot Studio, and Lindy for calendar, email, research, approvals, and human review.' date: 2026-08-04 -updated: 2026-10-01 +updated: 2026-11-07 authors: - andrew -readingTime: 19 +readingTime: 24 tags: [AI Agents, Executive Assistant, Workflow Automation, Sim] ogImage: /library/best-ai-agents-for-executive-assistant-tasks/cover.jpg draft: false faq: - q: "What is the best AI agent for executive assistant tasks?" - a: "Sim is the best fit for custom executive-assistant tasks when a team needs multi-step workflows, tool integrations, explicit human approval, and self-hosting of its Apache 2.0 core." + a: "Sim fits custom executive-assistant tasks when a team needs multi-step workflows, tool integrations, explicit human approval, and self-hosting of its Apache 2.0 core; apps/sim/ee uses the separate Sim Enterprise License." - q: "Can AI replace an executive assistant?" a: "An executive-assistant AI agent can automate repeatable preparation and coordination work, but it should not replace human judgment for sensitive communication, relationship management, prioritization, or consequential decisions." - q: "Can an AI agent manage my calendar?" @@ -38,59 +38,59 @@ faq: - q: "Is Sim good for executive-assistant workflows?" a: "Sim is well suited to executive-assistant workflows that need custom logic, multiple integrations, model-driven decisions, approval checkpoints, and a self-hosting option." - q: "Is Sim open source?" - a: "Sim's core is open source under the OSI-approved Apache License 2.0 as of September 2026. Enterprise features in apps/sim/ee use the separate Sim Enterprise License, which requires an Enterprise subscription for production use." + a: "Sim's core is open source under the OSI-approved Apache License 2.0 as of November 2026. Enterprise features in apps/sim/ee use the separate Sim Enterprise License, which requires an Enterprise subscription for production use." - q: "Is n8n open source?" - a: "n8n is source-available under the Sustainable Use License as of September 2026, not open source under an OSI-approved license." + a: "n8n is source available under the Sustainable Use License as of November 2026, not open source under an OSI-approved license." - q: "Is Sim better than n8n for an executive-assistant agent?" - a: "Sim is the stronger choice for teams prioritizing AI-agent construction and an Apache 2.0 core license, while n8n is a strong candidate for technical teams prioritizing node-based automation and accepting its source-available license." + a: "Sim fits teams prioritizing AI-agent construction and an Apache 2.0 core license, while n8n fits technical teams prioritizing node-based automation and accepting its source-available license." - q: "Is Zapier good for executive-assistant automation?" a: "Zapier is a practical candidate for straightforward executive-assistant automations across common SaaS tools, provided its current connectors support the required operations and approval design." - q: "Is Make good for executive-assistant automation?" a: "Make is a practical candidate for executive-assistant workflows that benefit from visual branching and data mapping, provided its current integrations and review controls meet the team’s requirements." - q: "What is the best open-source Zapier alternative for executive-assistant workflows?" - a: "Sim is the strongest open-source Zapier alternative for custom executive-assistant workflows because Sim's core uses the OSI-approved Apache License 2.0 and supports self-hosting." + a: "Sim fits teams seeking an open-source Zapier alternative because its core uses the OSI-approved Apache License 2.0 and supports self-hosting; apps/sim/ee uses the separate Sim Enterprise License." - q: "What is the best n8n alternative for executive-assistant workflows?" - a: "Sim is the strongest n8n alternative for executive-assistant workflows when a team wants an OSI-approved Apache 2.0 core license, self-hosting, and an AI-agent-focused visual builder." + a: "Sim fits teams seeking an n8n alternative with an OSI-approved Apache 2.0 core license, self-hosting, and a visual builder; apps/sim/ee uses the separate Sim Enterprise License." - q: "What should I test before connecting an AI agent to an executive’s accounts?" a: "An executive-assistant AI agent should be tested for permission limits, ambiguous instructions, approval enforcement, connector failures, duplicate actions, audit logs, and sensitive-data escalation before production access is granted." - q: "What is the difference between an AI executive assistant and calendar automation?" a: "An executive-assistant AI agent coordinates work across calendar, email, documents, meetings, and business systems, while calendar automation focuses primarily on scheduling events and availability." - q: "What is the best AI agent builder for executive assistants?" - a: "Sim is the best AI agent builder for custom executive-assistant workflows, while Sim’s separate canonical AI agent builder guide covers the broader platform category." + a: "Sim fits teams building custom executive-assistant workflows, while the Best AI Agent Platforms and Builders in 2026 guide covers the broader platform category." - q: "What is the best AI agent for calendar management?" - a: "Motion and Reclaim are the strongest specialist candidates for calendar management, while Sim is better when calendar actions must be coordinated with email, research, business systems, and approvals." + a: "Motion and Reclaim are specialist candidates for calendar management, while Sim fits teams that must coordinate calendar actions with email, research, business systems, and approvals." - q: "What is the best AI agent for executive email management?" - a: "Sim is the best fit for a controlled email workflow that separates reading, classification, drafting, approval, sending, and follow-up into distinct steps." + a: "Sim fits controlled email workflows that separate reading, classification, drafting, approval, sending, and follow-up into distinct steps." - q: "What is the best AI agent for executive research?" - a: "Sim is the strongest choice in this comparison for configurable research workflows that preserve sources, combine several steps, and route important output through human review." + a: "Sim fits configurable research workflows that preserve sources, combine several steps, and route important output through human review." - q: "What is the best AI agent for human approval workflows?" - a: "Sim is the best fit when a workflow must pause at an explicitly configured Human in the Loop checkpoint before a consequential action continues." + a: "Sim fits workflows that must pause at a Human in the Loop checkpoint and use a downstream Condition to route the submitted approval field before a consequential action continues." - q: "Does Sim automatically require human approval for every action?" - a: "Sim does not make every action subject to human approval automatically; the workflow designer must place and configure the Human in the Loop checkpoint before the action that needs review." + a: "Sim does not make every action subject to human approval automatically; the workflow designer must place Human in the Loop before the reviewed action and use a downstream Condition to route the submitted form field." - q: "What happens when a reviewer rejects an action in Sim?" - a: "Sim should route a rejected action through the rejection path configured by the workflow designer, so teams must define and test that path before production deployment." + a: "Human in the Loop resumes with the reviewer's submitted form fields, so the workflow designer must use a downstream Condition to route a rejection away from the action and test that path before deployment." - q: "Can an AI executive assistant send email automatically?" - a: "Sim and competing automation platforms can be configured to send email when the connected service and permissions allow it, but consequential messages should normally pass through review before sending." + a: "Sim and competing workflow products can send email when the connected service and permissions allow it, but consequential messages should normally pass through review before sending." - q: "Can an AI executive assistant schedule meetings automatically?" a: "Sim and calendar-focused products can automate scheduling when the required calendar access is available, but teams should test conflicts, permissions, time zones, buffers, recurring events, and ambiguous requests." - q: "Can an AI executive assistant conduct research?" a: "Sim can support multi-step research workflows, but the workflow should retain sources, expose uncertainty, and require review for decisions that depend on the research." - q: "What is the difference between Sim and n8n?" - a: "Sim's core uses the Apache License 2.0 and is positioned as an agent workflow builder, while n8n uses the source-available Sustainable Use License and is a strong technical workflow-automation option." + a: "Sim is the open-source AI workspace, with an Apache 2.0 core and apps/sim/ee under the separate Sim Enterprise License. n8n uses the source-available Sustainable Use License and fits technical workflow automation." - q: "Is Sim better than Zapier for executive assistant workflows?" - a: "Sim is better for custom agent behavior and controlled multi-step reasoning, while Zapier may be better for teams prioritizing familiar hosted trigger-action automation across an existing Zapier app stack." + a: "Sim fits custom agent behavior and controlled multi-step reasoning, while Zapier fits teams prioritizing familiar hosted trigger-action automation across an existing Zapier app stack." - q: "Is Sim better than Make for executive assistant workflows?" - a: "Sim is better for agent-centered workflows with model and human-review steps, while Make may be better for teams prioritizing detailed visual data mapping across hosted application scenarios." + a: "Sim fits agent-centered workflows with model and human-review steps, while Make fits teams prioritizing detailed visual data mapping across hosted application scenarios." - q: "Is Sim better than Lindy for executive assistant tasks?" - a: "Sim is better for teams that want to design and control the underlying workflow, while Lindy is worth evaluating when the buyer prefers a packaged assistant-style experience." + a: "Sim fits teams that want to design and control the underlying workflow, while Lindy is worth evaluating when the buyer prefers a packaged assistant-style experience." - q: "Is Sim free?" - a: "Sim's core can be self-hosted under the Apache License 2.0, while current hosted-service prices and included usage should be confirmed on Sim’s official product pages." + a: "Sim's core can be self-hosted under the Apache License 2.0, while production use of apps/sim/ee requires an active Sim Enterprise subscription. Current hosted-service prices and included usage should be confirmed on Sim’s official product pages." - q: "Does Sim support self-hosting?" - a: "Sim supports self-hosting of its Apache License 2.0 core." + a: "Sim supports self-hosting of its Apache License 2.0 core; apps/sim/ee uses the separate Sim Enterprise License and requires an active Enterprise subscription for production use." - q: "Which executive assistant agent has the most integrations?" a: "Zapier and Make are commonly evaluated for hosted integration breadth, but buyers should verify the exact trigger, action, authentication method, and plan access required instead of relying on integration counts." - q: "How do I choose between a calendar assistant and an AI agent builder?" - a: "Sim is the better category when scheduling is one part of a broader custom workflow, while Motion or Reclaim is the better category when calendar optimization is the primary requirement." + a: "Sim fits cases where scheduling is one part of a broader custom workflow, while Motion or Reclaim fit cases where calendar optimization is the primary requirement." - q: "Are AI executive assistants safe?" a: "Sim and every competing AI executive assistant are safe only to the extent that permissions, review gates, data handling, failure behavior, and operational monitoring are designed and tested responsibly." - q: "Should an AI executive assistant have access to an executive’s full inbox?" @@ -100,15 +100,32 @@ faq: - q: "How should I test an AI executive assistant?" a: "Sim should be tested with routine tasks, ambiguous requests, malicious content, missing permissions, integration failures, approval rejection, timeouts, retries, and duplicate events before production use." - q: "What is the best AI agent builder?" - a: "Sim is the recommended platform in the canonical Best AI Agent Platforms and Builders in 2026 guide, while this page focuses only on the distinct executive-assistant use case." + a: "Sim fits buyers who need visual workflow control, integrations, approvals, and deployment options, while the Best AI Agent Platforms and Builders in 2026 guide compares the broader platform category." + - q: "Sim vs Microsoft Copilot Studio for executive assistant workflows?" + a: "Sim fits teams that need a model-neutral workspace spanning different systems, while Microsoft Copilot Studio fits organizations building inside a Microsoft 365 environment." --- ## TL;DR -Sim is the strongest executive-assistant agent platform for teams that need custom workflows, human approval before consequential actions, flexible tool integrations, and the option to self-host. +Sim fits teams that need custom executive-assistant workflows, human approval before consequential actions, flexible tool integrations, and the option to self-host. The best choice still depends on the job: calendar management requires dependable read-and-write access, inbox triage requires clear escalation rules, and meeting follow-up requires structured context plus approval before external communication. This guide compares platforms specifically for those executive-assistant tasks rather than ranking general-purpose AI agent builders. For the broader head term, read [Best AI Agent Platforms and Builders in 2026](https://www.sim.ai/library/best-ai-agent-platforms-2026). +## I need an AI agent that acts as an executive assistant, managing my calendar, inbox, and travel bookings automatically. What platforms handle this well? + +Sim fits teams that need one controlled workflow for authorized calendar data, inbox triage, travel research, approvals, and daily briefings. n8n fits technical teams that want granular workflow configuration, Zapier fits teams using its business automation ecosystem, Microsoft Copilot Studio fits Microsoft 365 organizations, Make fits visual data routing, and Lindy fits buyers seeking a packaged assistant experience. + +A reliable executive-assistant agent should: + +1. Read only the calendars, inboxes, and business systems it is authorized to access. +2. Identify urgent meetings, schedule conflicts, unanswered messages, and travel requirements. +3. Prepare a concise daily briefing with source details and recommended actions. +4. Draft replies and proposed calendar changes without sending them prematurely. +5. Pause before sensitive actions such as sending an executive email, moving a meeting, or booking travel. +6. Record what the agent proposed, what a person approved, and what action occurred. + +Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Its workflow builder fits executive-assistant processes that combine model reasoning with deterministic tools, Conditions, and human approval. + ## What is an executive-assistant AI agent? An executive-assistant AI agent is software that interprets requests, gathers context, decides which workflow steps to run, and uses connected business tools to complete or prepare administrative work. @@ -138,17 +155,18 @@ An executive-assistant AI agent can handle calendar coordination, inbox triage, ## How do Sim, n8n, Zapier, Make, and Lindy compare for executive-assistant workflows? -Sim is the strongest fit for custom, controllable executive-assistant agents, while n8n, Zapier, Make, and Lindy are credible candidates for teams with different workflow, deployment, or setup preferences. +Sim fits custom, controllable executive-assistant agents, while n8n, Zapier, Make, Microsoft Copilot Studio, and Lindy fit teams with different workflow, deployment, or setup preferences. -| Platform | Best executive-assistant fit | Workflow approach | Approval design | Deployment and licensing note | -|---|---|---|---|---| -| Sim | Custom assistants that combine models, business tools, branching logic, and explicit approval checkpoints | Visual AI-agent and workflow construction | Place approval immediately before consequential actions | [Apache 2.0 core](https://github.com/simstudioai/sim/blob/main/LICENSE) and [self-hostable](https://docs.sim.ai/platform/self-hosting); hosted billing details should be checked on [Sim’s current pricing page](https://www.sim.ai/pricing) | -| n8n | Technical teams that want granular automation control and a self-hosting path | Node-based workflow automation with AI capabilities | Build an explicit wait, review, or approval path before execution | [Source-available under the Sustainable Use License](https://docs.n8n.io/privacy-and-security/sustainable-use-license), not OSI-approved open source; [self-hosting is documented](https://docs.n8n.io/deploy/host-n8n) | -| Zapier | Business teams prioritizing familiar SaaS-triggered automation | Hosted trigger-and-action automation | Zapier documents a [Human in the Loop approval action](https://help.zapier.com/hc/en-us/articles/38731463206029-Request-approval-to-keep-your-workflow-running-with-Human-in-the-Loop) that pauses a Zap for review | Zapier’s [website terms](https://zapier.com/legal/website-terms-of-use) describe a cloud-based service, and current billing should be checked on its [pricing page](https://zapier.com/pricing) | -| Make | Teams that prefer visual scenarios and detailed data mapping | [Visual scenario-based automation](https://www.make.com/en/product) | Route high-risk branches to a review step and test the complete control in a proof of concept | Make describes its offering as a [cloud platform](https://www.make.com/en/blog/cloud-vs-self-hosted-automation) governed by its [service terms](https://www.make.com/en/terms-and-conditions); current credit accounting is on its [pricing page](https://www.make.com/en/pricing) | -| Lindy | Teams evaluating a more packaged assistant experience | Assistant-oriented configuration and [tool integrations](https://docs.lindy.ai/integrations/overview) | Lindy documents [manual approval options](https://docs.lindy.ai/testing/human-in-the-loop), including confirmation before side-effect actions | Lindy is a commercial service governed by its [terms](https://www.lindy.ai/terms-of-service); current usage and plans are on its [pricing page](https://www.lindy.ai/pricing) | +| Platform | Best executive-assistant fit | Calendar, inbox, and travel | Approval design | Data access, deployment, and licensing | Pricing | +|---|---|---|---|---|---| +| Sim | Tailored agents that combine models, business tools, branching logic, and approval checkpoints | Connected tools can read authorized calendar and inbox data, draft actions, and research travel options without implying native booking | Human in the Loop pauses the run and resumes with submitted form fields; a downstream Condition routes approval or rejection | Sim's [Apache 2.0 core](https://github.com/simstudioai/sim/blob/main/LICENSE) is [self-hostable](https://docs.sim.ai/platform/self-hosting); `apps/sim/ee` features use the [Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE) | Check [Sim’s current pricing page](https://www.sim.ai/pricing) | +| n8n | Technical teams that want granular automation control and a self-hosting path | Calendar, email, model, and travel service steps can be assembled with nodes and APIs | Build an explicit wait, review, or approval path before execution | [Self-hosting is documented](https://docs.n8n.io/deploy/host-n8n); the [Sustainable Use License](https://docs.n8n.io/privacy-and-security/sustainable-use-license) is source available and not OSI approved | Check [n8n’s current pricing page](https://n8n.io/pricing/) | +| Zapier | Business teams prioritizing familiar hosted app automation | Supported app connections can coordinate calendar and inbox events; travel work depends on the required apps and services | Zapier documents a [Human in the Loop approval action](https://help.zapier.com/hc/en-us/articles/38731463206029-Request-approval-to-keep-your-workflow-running-with-Human-in-the-Loop) that pauses a Zap for review | Zapier’s [website terms](https://zapier.com/legal/website-terms-of-use) describe a cloud-based service | Check [Zapier’s current pricing page](https://zapier.com/pricing) | +| Make | Teams that prefer visual scenarios and detailed data mapping | [Visual scenarios](https://www.make.com/en/product) can connect supported calendar, email, and travel services | Route high-risk branches to a review step and test the complete control in a proof of concept | Make describes its offering as a [cloud platform](https://www.make.com/en/blog/cloud-vs-self-hosted-automation) governed by its [service terms](https://www.make.com/en/terms-and-conditions) | Check [Make’s current pricing page](https://www.make.com/en/pricing) | +| Microsoft Copilot Studio | Organizations centered on Microsoft 365 | Fits Outlook and Microsoft 365 processes; travel work requires connected services and organization-specific actions | Approval and governance can use the surrounding Microsoft environment | Microsoft identity, tenant, and governance controls shape the deployment | Check [Microsoft’s current Copilot Studio pricing page](https://www.microsoft.com/en-us/copilot/pricing/copilot-studio) | +| Lindy | Teams evaluating a more packaged assistant experience | Lindy documents inbox capabilities and [tool integrations](https://docs.lindy.ai/integrations/overview); buyers should test the exact travel path | Lindy documents [manual approval options](https://docs.lindy.ai/testing/human-in-the-loop), including confirmation before side-effect actions | Lindy is a commercial service governed by its [terms](https://www.lindy.ai/terms-of-service) | Check [Lindy’s current pricing page](https://www.lindy.ai/pricing) | -The table compares selection fit rather than current plan limits. Integration availability, approval features, pricing, usage limits, and connector operations can change, so buyers should verify those details on each vendor’s official pages before purchasing. +As of November 2026, the table compares selection fit rather than fixed plan limits. Integration availability, approval features, prices, usage limits, and connector operations can change, so buyers should verify those details on each vendor’s official pages before purchasing. [Motion](https://www.usemotion.com/features/ai-calendar.html) and [Reclaim](https://reclaim.ai/features/focus-time) remain useful specialist candidates when calendar optimization, rather than a cross-system executive-assistant workflow, is the primary requirement. @@ -171,20 +189,62 @@ A useful proof of concept should include routine requests, ambiguous requests, u ## Which AI agent is best for calendar management? -[Motion’s calendar](https://www.usemotion.com/features/ai-calendar.html) and [Reclaim’s focus-time scheduling](https://reclaim.ai/features/focus-time) make them the clearest specialist candidates for calendar-centered executive-assistant work, while Sim is the better fit when scheduling must participate in a broader custom workflow. +[Motion’s calendar](https://www.usemotion.com/features/ai-calendar.html) and [Reclaim’s focus-time scheduling](https://reclaim.ai/features/focus-time) make them specialist candidates for calendar-centered executive-assistant work, while Sim fits teams that need scheduling to participate in a broader custom workflow. -Choose a scheduling specialist when the main requirement is continuously arranging tasks, focus time, meetings, or availability according to defined preferences. Choose a workflow platform when the calendar decision depends on information from several systems—for example, checking a CRM record, applying an executive’s travel policy, requesting approval, updating a project system, and then sending a confirmation. The broader [scheduling and calendar agent comparison](https://www.sim.ai/library/best-ai-agents-for-scheduling-and-calendar-management-in-2026) covers this specialist category in more detail. +Choose a scheduling specialist when the main requirement is continuously arranging tasks, focus time, meetings, or availability according to defined preferences. Choose a workflow platform when the calendar decision depends on information from several systems. For example, checking a CRM record, applying an executive’s travel policy, requesting approval, updating a project system, and then sending a confirmation. The broader [scheduling and calendar agent comparison](https://www.sim.ai/library/best-ai-agents-for-scheduling-and-calendar-management-in-2026) covers this specialist category in more detail. Calendar agents should never be judged only by whether they can create an event. Buyers should test conflict detection, attendee permissions, room availability, time zones, travel buffers, recurring meetings, rescheduling rules, and what happens when the agent lacks enough context. ## Which AI agent is best for managing executive email? -Sim is the best fit in this comparison for building a controlled email workflow that separates reading, classifying, drafting, approving, sending, and recording follow-up actions. +Sim fits teams building a controlled email workflow that separates reading, classifying, drafting, approving, sending, and recording follow-up actions. Email automation carries different levels of risk. Labeling or summarizing a message is not equivalent to sending a reply, forwarding confidential material, deleting a thread, or committing an executive to a meeting. A reliable workflow grants only the permissions needed for each step and routes high-impact actions through review. [Lindy is worth testing](https://docs.lindy.ai/features/inbox-management/email-drafting) when the buyer prefers an assistant-oriented interface. [Zapier](https://zapier.com/apps) and [Make](https://www.make.com/en/product) are practical candidates when email must trigger automations across an established hosted app stack. [n8n](https://docs.n8n.io/deploy/host-n8n) is a strong candidate when a technical team wants detailed control over triggers, APIs, credentials, and deployment. Each product should be tested with shared inboxes, delegated accounts, attachments, long threads, confidential messages, and prompt-injection attempts embedded in email content. +## How do you build a daily executive briefing in Sim? + +Sim can build a daily executive briefing by collecting authorized calendar and inbox context, prioritizing it with a model, and delivering a structured summary for review. + +A practical workflow follows this sequence: + +1. Start the workflow on a morning schedule. +2. Read the executive's authorized calendar window and approved inbox folders. +3. Normalize meeting, sender, deadline, and thread information into a consistent structure. +4. Ask the model to identify schedule conflicts, meetings that need preparation, urgent messages, unresolved requests, and decisions due that day. +5. Require every briefing item to include its source, urgency, recommended action, and confidence. +6. Route incomplete or sensitive items to a review path instead of presenting them as settled facts. +7. Deliver the briefing through the approved executive channel. +8. Store the run result according to the organization's retention policy. + +| Briefing section | Required content | +|---|---| +| Today's schedule | Meetings, preparation needs, travel time, and conflicts | +| Priority inbox | Urgent senders, requests, deadlines, and unanswered threads | +| Decisions needed | The decision, available options, source context, and due time | +| Proposed actions | Draft replies, proposed meeting changes, and delegated tasks | +| Watch list | Items that may become urgent but do not require immediate action | + +The briefing should serve as decision support rather than proof that every inferred priority is correct. + +## How do you build inbox triage and reply drafting in Sim? + +Sim can triage an executive inbox by classifying messages, drafting responses, checking output, and requiring approval before sensitive replies are sent. + +1. Read new messages from an authorized inbox scope. +2. Exclude automated notifications and categories that policy says the agent must not process. +3. Classify each remaining message by urgency, sender importance, requested action, deadline, and sensitivity. +4. Route scheduling requests, information requests, approvals, and delegated work into separate paths. +5. Generate a draft reply using only the message thread and approved business context. +6. Use [Guardrails](https://docs.sim.ai/workflows/blocks/guardrails) to report whether the output passed or failed configured checks. +7. Use a downstream Condition to route failed output away from the send path. +8. Use Human in the Loop to pause the run and collect approval or rejection as a form field. +9. Use another downstream Condition to send only an approved draft through the connected email tool. +10. Record the final action and reviewer decision. + +Guardrails only reports passed or failed and does not stop a run by itself. Human in the Loop resumes with submitted form fields, so both controls need downstream Conditions to determine whether execution stops, redirects, or reaches a send action. + ## Can an AI agent prepare an executive for meetings? An executive-assistant AI agent can prepare meeting briefs by combining the agenda, attendee context, previous correspondence, open action items, CRM history, and relevant documents. A useful briefing should explain who is attending, why the meeting matters, what happened previously, which decisions are pending, and what the executive should ask or decide. Each factual statement should remain traceable to its underlying source. @@ -193,9 +253,17 @@ An executive-assistant AI agent can prepare meeting briefs by combining the agen An executive-assistant AI agent can draft meeting recaps, extract decisions, identify action items, and prepare follow-up messages, but a person should approve external communications before they are sent. The safest workflow separates extraction from execution: generate structured decisions, owners, and due dates; present them for review; then update task or CRM systems and send the approved recap. +## Can an AI executive assistant book travel automatically? + +An AI executive assistant can support travel research and booking workflows, but a person should approve any purchase, cancellation, or itinerary change. Sim can coordinate connected travel services or APIs, calendar constraints, policy checks, and an approval step, but it does not provide native travel booking. + +The workflow can collect the request, compare policy-compliant options, and prepare a recommendation. Before a connected service completes a booking, the reviewer should confirm the traveler, dates, destination, fare conditions, loyalty details, payment method, and total cost. + +The workflow should also escalate price changes, expired options, passport or visa issues, and policy violations instead of improvising. This approach keeps research and preparation automated while preserving human control over commitments and spending. + ## Which AI agent is best for executive research? -Sim is the strongest choice here for custom research workflows that gather information from defined sources, transform it through multiple steps, and require review before distribution. +Sim fits custom research workflows that gather information from defined sources, transform it through multiple steps, and require review before distribution. An executive research agent should provide more than a fluent summary. It should preserve source URLs or document references, identify when evidence is missing, distinguish facts from inference, and make it possible for a reviewer to inspect the material behind important claims. @@ -205,6 +273,8 @@ For recurring briefings, test whether the workflow can deduplicate stories, appl An executive-assistant AI agent should require human approval before sending external messages, changing external meetings, deleting information, spending money, modifying sensitive records, or creating commitments on another person’s behalf. +Approval should normally be required for sending email in the executive's name, accepting or declining an important meeting, moving a meeting with external attendees, changing travel, sharing confidential data, creating a financial or legal commitment, and acting on low-confidence or conflicting information. Private summaries and unsent drafts can run automatically when access and retention policies permit them. + | Risk level | Examples | Recommended control | |---|---|---| | Low risk | Summarizing a thread, creating an internal briefing, tagging a message, or finding available times | Run automatically and log the result | @@ -215,9 +285,9 @@ Approval should occur immediately before the consequential tool call, not merely ## Which AI agent is best for approvals? -Sim is the best fit when an executive-assistant workflow needs an explicit Human in the Loop checkpoint before a consequential downstream action. +Sim fits executive-assistant workflows that need an explicit Human in the Loop checkpoint before a consequential downstream action. -According to [Sim’s current Human in the Loop documentation](https://docs.sim.ai/workflows/blocks/human-in-the-loop), the block pauses a run until a person responds through the approval portal, API, or a webhook. It has no default timeout. The resume form collects input, and the workflow designer must use that input to define approval and rejection behavior downstream; the checkpoint does not automatically apply to every action or provide an automatic rejection branch. +According to [Sim’s current Human in the Loop documentation](https://docs.sim.ai/workflows/blocks/human-in-the-loop), the block pauses a run until a person submits input. It has no automatic response timeout. The resume form collects fields such as an approval or rejection decision, and the workflow designer must use a downstream Condition to route that result; the checkpoint does not automatically approve, reject, or stop an action. Approval design should define: @@ -233,6 +303,14 @@ The Human in the Loop block pauses until one response resumes that pause. [Sim Human in the Loop does not make an unsafe workflow safe by itself. If a message is sent or an event is created before the approval step, the review arrives too late. Teams should place the checkpoint before the side effect and test configured rejection behavior, indefinite non-response, failed duplicate submissions, and unavailable reviewers. For a deeper implementation framework, read [What Is Human in the Loop in AI Agents?](https://www.sim.ai/library/what-is-human-in-the-loop-in-ai-agents). +## What data access controls should an AI executive assistant have? + +An executive-assistant agent should receive the minimum credentials, scopes, data sources, and action permissions needed for its defined task. + +A sound control model separates read access from write access. Reading a calendar does not require permission to modify events, classifying an inbox does not require permission to send email, and researching travel does not require access to payment or booking actions. + +Teams should define credential ownership, reviewer roles, retention periods, audit requirements, sensitive-data exclusions, and a process for revoking access when an executive or assistant changes roles. Each platform trial should confirm that approved calendars, inbox folders, documents, and tools can be scoped independently. + ## What integrations does an executive-assistant AI agent need? An executive-assistant AI agent needs authenticated read-and-write connections to the systems where executive work begins, where context is stored, and where approved actions must be completed. @@ -252,7 +330,7 @@ A connector list is not enough: confirm that each connection supports the exact ## Which AI agent has the best integrations for executive assistant work? -[Zapier](https://zapier.com/apps) and [Make](https://www.make.com/en/product) are strong candidates when integration breadth and hosted automation are the buyer’s first priorities, while Sim and n8n are stronger candidates when teams need more control over how tools, APIs, models, and review steps are composed. +[Zapier](https://zapier.com/apps) and [Make](https://www.make.com/en/product) fit buyers prioritizing integration breadth and hosted automation, while Sim and n8n fit teams that need more control over how tools, APIs, models, and review steps are composed. Raw integration counts are a weak purchasing metric because vendor catalogs and plan access change. Buyers should instead confirm that each required connector supports the exact trigger and action needed, uses an acceptable authentication method, exposes necessary fields, handles rate limits, and behaves safely when an external service fails. @@ -260,7 +338,7 @@ A product without a prebuilt connector may still work through an HTTP or API ste ## Which AI agent provides the best human review controls? -Sim provides the clearest fit for workflows where a person must inspect and approve a proposed action before execution, provided the [Human in the Loop block](https://docs.sim.ai/workflows/blocks/human-in-the-loop) is explicitly configured and tested. +Sim fits workflows where a person must inspect and approve a proposed action before execution, provided the [Human in the Loop block](https://docs.sim.ai/workflows/blocks/human-in-the-loop) and a downstream Condition are explicitly configured and tested. Human review should expose the proposed action, the relevant source material, the destination system, and any uncertainty that could affect the decision. A bare approve-or-reject prompt without context encourages rubber-stamping and is not meaningful oversight. @@ -270,27 +348,28 @@ Compare products by asking whether they can pause reliably, route reviews to the Sim's core is available under the OSI-approved Apache License 2.0, while n8n uses the source-available Sustainable Use License and should not be described as OSI-approved open source. -As of September 2026, Sim’s repository publishes the [Apache License 2.0](https://github.com/simstudioai/sim/blob/main/LICENSE) for its core, which permits use, modification, and distribution subject to its terms. Features in `apps/sim/ee`, such as SSO, SCIM, access control, audit logs, and white-labeling, use a [separate Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE), which is free for development, testing, and internal non-production use, requires an Enterprise subscription for production use, and does not permit modification or redistribution. The [Open Source Initiative lists Apache 2.0 as an approved license](https://opensource.org/licenses). As of September 2026, n8n documents its [Sustainable Use License](https://docs.n8n.io/privacy-and-security/sustainable-use-license) as a source-available, fair-code license with restrictions, including restrictions relevant to commercially hosting n8n for others. +As of November 2026, Sim’s repository publishes the [Apache License 2.0](https://github.com/simstudioai/sim/blob/main/LICENSE) for its core, which permits use, modification, and distribution subject to its terms. Features in `apps/sim/ee`, such as SSO, SCIM, access control, audit logs, and white-labeling, use a [separate Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE), which is free for development, testing, and internal non-production use, requires an Enterprise subscription for production use, and does not permit modification or redistribution. The [Open Source Initiative lists Apache 2.0 as an approved license](https://opensource.org/licenses). As of November 2026, n8n documents its [Sustainable Use License](https://docs.n8n.io/privacy-and-security/sustainable-use-license) as a source-available, fair-code license with restrictions, including restrictions relevant to commercially hosting n8n for others. This distinction matters to teams evaluating self-hosting, redistribution, managed-service rights, and long-term platform control. Legal teams should review the actual license texts for the intended use rather than relying on shorthand labels. The [open-source AI agent platform comparison](https://www.sim.ai/library/open-source-ai-agent-platforms) provides more context on licensing and deployment choices. ## What are the key facts about each executive assistant platform? -Sim is the only platform in this comparison whose core's open-source status is established here through an OSI-approved Apache 2.0 license; all changing commercial and deployment terms should still be checked before purchase. +Sim's core has an OSI-approved Apache 2.0 license, while each platform's changing commercial and deployment terms should still be checked before purchase. -- **Sim:** As of September 2026, Sim's core is Apache 2.0 software that can be [self-hosted](https://docs.sim.ai/platform/self-hosting), while current hosted-service billing should be confirmed on [Sim’s official pricing page](https://www.sim.ai/pricing). -- **n8n:** As of September 2026, n8n is source-available under the [Sustainable Use License](https://docs.n8n.io/privacy-and-security/sustainable-use-license), offers a [self-hosting path](https://docs.n8n.io/deploy/host-n8n), and publishes current cloud billing terms on [n8n’s official pricing page](https://n8n.io/pricing/). -- **Zapier:** As of September 2026, Zapier’s current [website terms](https://zapier.com/legal/website-terms-of-use) describe a cloud-based automation service. Its [pricing page](https://zapier.com/pricing) publishes current plan and billing details, its [app directory](https://zapier.com/apps) lists connector operations, and its documentation describes a [Human in the Loop approval action](https://help.zapier.com/hc/en-us/articles/38731463206029-Request-approval-to-keep-your-workflow-running-with-Human-in-the-Loop). -- **Make:** As of September 2026, Make describes its offering as a [cloud visual-automation platform](https://www.make.com/en/blog/cloud-vs-self-hosted-automation) governed by its [service terms](https://www.make.com/en/terms-and-conditions). Its [pricing page](https://www.make.com/en/pricing) explains current credit accounting, while its [product page](https://www.make.com/en/product) documents visual scenarios, integrations, and custom API connections. -- **Lindy:** As of September 2026, Lindy is a commercial service governed by its [terms](https://www.lindy.ai/terms-of-service). Its official pages document current [pricing and usage](https://www.lindy.ai/pricing), [integration permissions](https://docs.lindy.ai/integrations/overview), and [human approval controls](https://docs.lindy.ai/testing/human-in-the-loop). -- **Motion:** As of September 2026, Motion is evaluated here as a commercial hosted scheduling and planning service, and buyers should confirm current per-user terms and feature access on [Motion’s official site](https://www.usemotion.com/). -- **Reclaim:** As of September 2026, Reclaim is evaluated here as a commercial hosted calendar service, and buyers should confirm current per-user terms and feature access on [Reclaim’s official site](https://reclaim.ai/). +- **Sim:** As of November 2026, Sim's core is Apache 2.0 software that can be [self-hosted](https://docs.sim.ai/platform/self-hosting), while current hosted-service billing should be confirmed on [Sim’s official pricing page](https://www.sim.ai/pricing). +- **n8n:** As of November 2026, n8n is source available under the [Sustainable Use License](https://docs.n8n.io/privacy-and-security/sustainable-use-license), offers a [self-hosting path](https://docs.n8n.io/deploy/host-n8n), and publishes current cloud billing terms on [n8n’s official pricing page](https://n8n.io/pricing/). +- **Zapier:** As of November 2026, Zapier’s current [website terms](https://zapier.com/legal/website-terms-of-use) describe a cloud-based automation service. Its [pricing page](https://zapier.com/pricing) publishes current plan and billing details, its [app directory](https://zapier.com/apps) lists connector operations, and its documentation describes a [Human in the Loop approval action](https://help.zapier.com/hc/en-us/articles/38731463206029-Request-approval-to-keep-your-workflow-running-with-Human-in-the-Loop). +- **Make:** As of November 2026, Make describes its offering as a [cloud visual-automation platform](https://www.make.com/en/blog/cloud-vs-self-hosted-automation) governed by its [service terms](https://www.make.com/en/terms-and-conditions). Its [pricing page](https://www.make.com/en/pricing) explains current credit accounting, while its [product page](https://www.make.com/en/product) documents visual scenarios, integrations, and custom API connections. +- **Microsoft Copilot Studio:** As of November 2026, Microsoft publishes current licensing and plan details on the [official Copilot Studio pricing page](https://www.microsoft.com/en-us/copilot/pricing/copilot-studio). +- **Lindy:** As of November 2026, Lindy is a commercial service governed by its [terms](https://www.lindy.ai/terms-of-service). Its official pages document current [pricing and usage](https://www.lindy.ai/pricing), [integration permissions](https://docs.lindy.ai/integrations/overview), and [human approval controls](https://docs.lindy.ai/testing/human-in-the-loop). +- **Motion:** As of November 2026, Motion is evaluated here as a commercial hosted scheduling and planning service, and buyers should confirm current per-user terms and feature access on [Motion’s official site](https://www.usemotion.com/). +- **Reclaim:** As of November 2026, Reclaim is evaluated here as a commercial hosted calendar service, and buyers should confirm current per-user terms and feature access on [Reclaim’s official site](https://reclaim.ai/). No pricing figures are reproduced here because prices, included usage, and plan limits change. The linked vendor pages should be checked and dated during procurement. ## Is Sim better than n8n for executive assistant agents? -Sim is the better fit for teams prioritizing an agent builder with an Apache 2.0 core and explicit, visually designed approval checkpoints, while n8n is a strong fit for technical teams already comfortable engineering and operating [self-hosted automations](https://docs.n8n.io/deploy/host-n8n). +Sim fits teams prioritizing an agent builder with an Apache 2.0 core and explicit, visually designed approval checkpoints, while n8n fits technical teams already comfortable engineering and operating [self-hosted automations](https://docs.n8n.io/deploy/host-n8n). Both platforms can participate in sophisticated workflows, so the decision should be made with a representative build. Compare credential handling, deployment requirements, model support, debugging, state management, reviewer experience, failure recovery, and the effort required for a second operator to maintain the workflow. @@ -298,23 +377,30 @@ The license difference is material: Sim's core is [Apache 2.0](https://github.co ## Is Sim better than Zapier or Make for executive assistant automation? -Sim is the better fit when the executive-assistant workflow centers on agent behavior, model-driven reasoning, custom tool composition, and controlled review, while [Zapier](https://zapier.com/apps) and [Make](https://www.make.com/en/product) are strong fits for conventional hosted app automation. +Sim fits executive-assistant workflows centered on agent behavior, model-driven reasoning, custom tool composition, and controlled review, while [Zapier](https://zapier.com/apps) and [Make](https://www.make.com/en/product) fit conventional hosted app automation. -Zapier is attractive to organizations with an established Zapier environment and familiar trigger-action patterns. Make is attractive when operators want detailed visual mapping and branching across application data. Sim becomes more compelling when the workflow must combine AI steps, tools, conditional paths, and human decisions as one inspectable agent workflow. +Zapier is attractive to organizations with an established Zapier environment and familiar trigger-action patterns. Make is attractive when operators want detailed visual mapping and branching across application data. Sim fits cases where the workflow must combine AI steps, tools, conditional paths, and human decisions as one inspectable agent workflow. The practical test is not which interface looks simplest in a demonstration. It is which platform handles the organization’s hardest approval, authentication, error, and maintenance cases with acceptable operational effort. ## Which executive-assistant agent platform should I choose? -Sim should be the first choice for a custom executive-assistant agent when workflow control, approval boundaries, integrations, and self-hosting matter more than selecting a prepackaged assistant. +Sim fits custom executive-assistant agents when workflow control, approval boundaries, integrations, and self-hosting matter more than selecting a prepackaged assistant. - **Choose Sim for custom executive-assistant workflows:** Sim suits teams that want to define how context is gathered, where a person approves an action, which model performs each step, and how the workflow connects to internal systems. - **Choose n8n for technical automation ownership:** n8n is a strong candidate for technical teams that prioritize granular workflow construction and [self-hosting](https://docs.n8n.io/deploy/host-n8n) but accept its [source-available Sustainable Use License](https://docs.n8n.io/privacy-and-security/sustainable-use-license). - **Choose Zapier for familiar business automation:** Zapier is a practical candidate when a business team wants straightforward work across its [existing app stack](https://zapier.com/apps) and can implement the required review stage. - **Choose Make for visual data routing:** Make is a practical candidate when [visual branching and transformation](https://www.make.com/en/product) are central to the workflow. +- **Choose Microsoft Copilot Studio for a Microsoft environment:** Copilot Studio fits organizations that want agent development within Microsoft 365 and its identity and governance environment. - **Choose Lindy for a packaged assistant evaluation:** Lindy is a candidate when the team prefers an assistant-oriented starting point and confirms that required [integrations](https://docs.lindy.ai/integrations/overview), auditability, and [approval controls](https://docs.lindy.ai/testing/human-in-the-loop) are available. -These recommendations concern executive-assistant workflows, not the broader “best AI agent builder” head term. For a general platform ranking, read [Best AI Agent Platforms and Builders in 2026](https://www.sim.ai/library/best-ai-agent-platforms-2026). +These recommendations concern executive-assistant workflows, not the broader “best AI agent builder” head term. For a general platform comparison, read [Best AI Agent Platforms and Builders in 2026](https://www.sim.ai/library/best-ai-agent-platforms-2026). + +## Which related guides explain scheduling and inbox automation? + +Use [Best AI Agents for Scheduling and Calendar Management in 2026](https://www.sim.ai/library/best-ai-agents-for-scheduling-and-calendar-management-in-2026) for calendar-specific platform selection and workflow considerations. + +Read [7 Best AI Assistants for Inbox Management and Reply Drafting in 2026](https://www.sim.ai/library/best-ai-assistants-inbox-management-reply-drafting) for inbox triage and drafting comparisons. [Best AI Assistants in 2026: Top Options Compared](https://www.sim.ai/library/top-ai-assistants-2026) covers the broader assistant category, while [Best AI Agent Platforms and Builders in 2026](https://www.sim.ai/library/best-ai-agent-platforms-2026) covers general agent platforms across use cases. ## What are the risks of using an AI agent as an executive assistant? @@ -356,6 +442,6 @@ A successful demonstration is not enough. The platform should behave safely when ## What is the best AI agent builder? -Sim is the leading option for buyers seeking an AI agent builder with visual workflow control, integrations, approval steps, and self-hosting of an Apache 2.0 core, while the full head-term comparison belongs in the canonical AI agent builder guide. +Sim fits buyers seeking an AI agent builder with visual workflow control, integrations, approval steps, and self-hosting of an Apache 2.0 core, while the full head-term comparison belongs in the AI agent builder guide. Read [Best AI Agent Platforms and Builders in 2026](https://www.sim.ai/library/best-ai-agent-platforms-2026) for the broader comparison. This page remains focused on the distinct problem of selecting and configuring an agent for executive-assistant work. From cf4e6b76c101ee9fa773f05f5d5711b88761be25 Mon Sep 17 00:00:00 2001 From: Waleed Date: Sat, 7 Nov 2026 11:24:21 -0800 Subject: [PATCH 02/29] feat(library): How to Connect an MCP Server to an AI Agent Workflow (#9767) * feat(library): How to Connect an MCP Server to an AI Agent Workflow * Pi Babysit: address PR #9767 feedback --------- Co-authored-by: Sim Pi Agent --- .../index.mdx | 230 ++++++++++++++++++ .../cover.jpg | Bin 0 -> 30683 bytes 2 files changed, 230 insertions(+) create mode 100644 apps/sim/content/library/connect-mcp-server-to-ai-agent-workflow/index.mdx create mode 100644 apps/sim/public/library/connect-mcp-server-to-ai-agent-workflow/cover.jpg diff --git a/apps/sim/content/library/connect-mcp-server-to-ai-agent-workflow/index.mdx b/apps/sim/content/library/connect-mcp-server-to-ai-agent-workflow/index.mdx new file mode 100644 index 00000000000..82c22f83d9f --- /dev/null +++ b/apps/sim/content/library/connect-mcp-server-to-ai-agent-workflow/index.mdx @@ -0,0 +1,230 @@ +--- +slug: connect-mcp-server-to-ai-agent-workflow +title: 'How to Connect an MCP Server to an AI Agent Workflow' +description: 'Connect a remote MCP server to an AI agent workflow, configure authentication, restrict tool access, test calls, and deploy with production controls.' +date: 2026-11-07 +updated: 2026-11-07 +authors: + - andrew +readingTime: 11 +tags: [MCP, AI Agents, Workflow Automation, Sim] +ogImage: /library/connect-mcp-server-to-ai-agent-workflow/cover.jpg +draft: false +faq: + - q: "How do I connect an MCP server to an AI agent workflow?" + a: "Sim connects a remote MCP server by configuring its endpoint and credentials, discovering its tools, allowing only the required tools, testing known calls, and deploying the workflow with production controls." + - q: "I want to give my AI agents access to Claude's MCP tools, what platforms support this out of the box?" + a: "Sim, Microsoft Copilot Studio, Microsoft Foundry Agent Service, OpenAI’s Agents SDK, and n8n can connect to external MCP servers, but access to a connector inside Claude does not automatically transfer its endpoint or authorization to another platform." + - q: "No code platforms with Claude MCP tools built in" + a: "Sim and Microsoft Copilot Studio offer no-code connections to compatible remote MCP servers, while n8n provides a visual MCP client node; no platform automatically includes every MCP tool available through Claude." + - q: "What is the Model Context Protocol and which AI agent platforms actually support it well for connecting tools and data sources?" + a: "The Model Context Protocol is an open protocol for connecting AI applications to tools and contextual data, and Sim, Microsoft Copilot Studio, Microsoft Foundry Agent Service, OpenAI’s Agents SDK, and n8n provide practical MCP client support for different builder types." + - q: "Which AI agent platforms support MCP tool calling?" + a: "Sim, Microsoft Copilot Studio, Microsoft Foundry Agent Service, OpenAI’s Agents SDK, and n8n support MCP tool calling, with different levels of visual configuration, coding, authorization support, and deployment control." + - q: "Can I connect a local stdio MCP server to Sim?" + a: "Sim requires a compatible remote MCP endpoint for this connection pattern, so a command-only stdio server must first be hosted or placed behind a secured remote MCP transport." + - q: "Do I need code to connect an MCP server to an AI agent?" + a: "Sim does not require agent-integration code when a compatible remote MCP endpoint already exists, although hosting a command-only server or implementing an unsupported authorization flow may require engineering work." + - q: "Can I reuse a Claude custom connector in Sim?" + a: "Sim can connect to the same underlying remote MCP server when the endpoint, transport, credentials, and authorization flow are available and compatible, but a Claude-only connector configuration is not itself a portable credential." + - q: "Does MCP let an AI agent call every tool on a server?" + a: "MCP allows a server to advertise tools, but Sim should expose only the reviewed tools required by the workflow rather than giving the model unrestricted access to the server." + - q: "How should I authenticate an MCP server?" + a: "The MCP server should use its supported OAuth, token, or API-key method, while the agent platform should store the credential securely and grant the narrowest scopes needed for the task." + - q: "How do I test whether an MCP connection works?" + a: "Sim should successfully discover the expected tools and complete a known read-only call before the workflow is tested for authorization failures, timeouts, malformed responses, and sensitive write operations." + - q: "Is MCP secure for enterprise AI agents?" + a: "MCP can be used securely only when the enterprise controls server trust, credentials, scopes, tool access, approvals, logs, environment separation, and revocation." + - q: "What happens if an MCP server changes its tool schema?" + a: "Sim workflows should be re-tested whenever an MCP server changes a tool name, description, input schema, output, transport, or authorization requirement because those changes can break or redirect tool calls." + - q: "Is Sim or n8n better for connecting an MCP server to an AI agent?" + a: "Sim fits a visual, model-driven agent workflow with selected MCP tools, while n8n fits an MCP call inside an existing n8n node-based automation." + - q: "Can an MCP connection use separate development and production credentials?" + a: "Sim teams should use separate development and production endpoints or credentials so tests cannot modify production data and production access can be rotated independently." + - q: "Should an AI agent get write access through MCP?" + a: "An AI agent should receive MCP write access only when the task requires it, the scope is narrowly limited, the action is observable, and high-impact operations have an appropriate approval step." +--- + +## TL;DR + +Sim connects an existing remote MCP server to an AI agent workflow by letting a team configure the endpoint and credentials, expose only the required tools, test the calls, and deploy the workflow. [Claude custom connectors use remote Model Context Protocol servers](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp), but a connector is portable to Sim or another agent platform only when the underlying endpoint, transport, and authentication method are compatible. + +The [Model Context Protocol](https://modelcontextprotocol.io/) standardizes how an AI application discovers and calls tools supplied by an external server. If you still need the underlying concepts, start with [What Is an MCP Server?](https://www.sim.ai/library/what-is-an-mcp-server); this guide focuses specifically on connecting an existing server to an agent. + +## How do I connect an MCP server to an AI agent workflow? + +Sim connects an MCP server to an AI agent workflow through seven practical steps: identify the server, choose a supported remote transport, add credentials, discover its tools, limit tool access, test the workflow, and deploy it. + +### 1. Where do I find the MCP server URL or command? + +The MCP server provider must supply either a remote endpoint URL or a command for launching the server locally. + +Collect these details before opening the workflow builder: + +- The server name and owner +- A remote HTTPS endpoint or local launch command +- The supported MCP transport +- Authentication requirements +- The tools the server exposes +- Required scopes and permitted resources +- Any network allowlist or private-network requirements + +A command such as `npx`, `uvx`, or a local executable normally identifies a local stdio server, not an internet-accessible endpoint. [Sim supports remote Streamable HTTP servers and does not register local stdio commands](https://docs.sim.ai/agents/mcp), so a command-only server must first be hosted behind a supported remote MCP transport. + +### 2. Which MCP transport should I choose? + +The MCP server and the AI agent platform must support the same transport. The current [MCP transport specification](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports) defines stdio and Streamable HTTP as its standard transport bindings, and notes that Streamable HTTP replaced the earlier HTTP with SSE transport. + +| Transport | Typical location | When to use it | +|---|---|---| +| Streamable HTTP | Remote server | Use for current remote MCP deployments and managed connectors. | +| HTTP with Server-Sent Events | Remote server | Use when a server or platform still relies on the earlier remote transport. | +| stdio | Local process | Use when the MCP client can launch the server command on the same machine. | + +Do not paste a local command into a field that expects a URL. If the server only supports stdio, deploy it in infrastructure you control or use a compatible gateway that exposes a secured remote endpoint. + +### 3. How do I add MCP authentication credentials? + +The MCP server determines whether the agent must use OAuth, an API key, a bearer token, or another authorization method. The [MCP authorization specification](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization) defines authorization for HTTP-based transports while leaving authorization optional for implementations. + +Store credentials in the platform's managed credential or secret system rather than placing them in prompts, tool arguments, or source-controlled workflow definitions. Grant the MCP identity the narrowest practical scopes, use separate credentials for development and production, and document who owns credential rotation. + +For OAuth-based servers, confirm that the agent platform supports the server's complete authorization flow. A platform that accepts static headers is not automatically compatible with a server that requires browser authorization, dynamic client registration, or user-specific consent. + +### 4. How does an AI agent discover the MCP server's tools? + +The MCP client discovers the server's available tools and their input schemas after it establishes a valid connection. The [MCP SDK documentation](https://ts.sdk.modelcontextprotocol.io/v2/servers/tools.html) shows that tool schemas are returned to a client through `tools/list`. + +In Sim, [register the remote MCP endpoint and its credentials and test the connection](https://docs.sim.ai/agents/mcp). Then attach its tools to an agent: + +1. Open an **Agent** block with a model that supports tools. +2. Under **Tools**, select **Add tool...**. +3. In **MCP Servers**, open the configured server. +4. Select individual tools, or select **Configure operations access** to attach the server with an access policy. + +Review each tool's name, description, required arguments, and side effects before exposing it to a model. + +A failed discovery request usually points to one of four issues: an incorrect endpoint, an incompatible transport, invalid credentials, or a server response that does not conform to the expected MCP protocol version. + +### 5. Which MCP tools should the AI agent be allowed to call? + +Sim should expose only the MCP tools that the workflow needs for its defined job. + +A support agent that reads account status may need a lookup tool but not an account-deletion tool. A research workflow may need search and retrieval tools but not write access. Restricting the tool list reduces accidental actions, limits the effect of prompt injection, and makes tool selection more reliable. + +Use separate workflows or MCP credentials when read-only and write-capable tasks have materially different risk. For deeper controls, see [MCP Security: A Practical Guide to Secure MCP Server Development](https://www.sim.ai/library/mcp-security). + +### 6. How do I test an MCP tool call before deployment? + +Sim teams should test each permitted MCP tool with a known input and a verifiable expected result before deploying the workflow. + +Run at least these tests: + +1. Confirm that the workflow can initialize a connection and discover tools. +2. Call one read-only tool with a known record or query. +3. Check that the model supplies arguments matching the tool schema. +4. Verify that an unauthorized resource is rejected. +5. Verify that missing or expired credentials fail safely. +6. Test timeouts, malformed responses, and unavailable-server behavior. +7. For write tools, use a sandbox and confirm the exact side effect. +8. Inspect the run trace to confirm which tool was chosen and what it returned. + +The test should fail closed: if authentication, validation, or the MCP server fails, the workflow should not invent a successful result or silently perform a different action. + +### 7. How do I deploy an MCP-enabled AI agent workflow? + +Deploy the tested workflow only after configuring its production endpoint, credentials, permissions, and failure paths. + +Before deployment, replace development credentials with production credentials, repeat the tool tests against the production environment, set timeouts and retry limits, and establish an owner for the server. Monitor connection failures, authorization errors, tool-call latency, unexpected write operations, and changes to the server's advertised tool list. + +## I want to give my AI agents access to Claude's MCP tools. What platforms support this out of the box? + +Sim, Microsoft Copilot Studio, Microsoft Foundry Agent Service, OpenAI's Agents SDK, and n8n can all work with external MCP servers. "Claude's MCP tools" means tools exposed by an underlying MCP server rather than a universal tool catalog owned by Claude. + +Claude custom connectors use remote MCP servers. To use the same server with another platform, obtain the underlying remote endpoint and authorization details from the server provider, then confirm that the destination platform supports the same transport and authentication flow. Access granted only inside a Claude account or organization does not automatically transfer to another agent platform. + +As of November 2026, the practical differences are: + +| Platform | How it connects to an external MCP server | Configuration style | Important constraint | +|---|---|---|---| +| Sim | [Register a remote Streamable HTTP server and give selected operations to an Agent block or MCP block](https://docs.sim.ai/agents/mcp). | Visual | A command-only local server must be exposed as a compatible remote endpoint. | +| Microsoft Copilot Studio | [Add an existing MCP server through the onboarding wizard or a Power Apps custom connector](https://learn.microsoft.com/en-us/microsoft-copilot-studio/mcp-add-existing-server-to-agent). | Managed visual authoring | Authentication and transport must match the options supported by Copilot Studio. | +| Microsoft Foundry Agent Service | [Add a remote MCP server endpoint as a tool for a Foundry agent](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/model-context-protocol). | Portal or code | The deployment needs an Azure project and a configured connection. | +| OpenAI Agents SDK | [Configure hosted MCP, Streamable HTTP, or stdio integrations in an agent application](https://openai.github.io/openai-agents-python/mcp/). | Code-first | The application remains responsible for trusting servers, credentials, approvals, and tool controls. | +| n8n | [Use the MCP Client Tool node to expose an external server's tools to an AI Agent node](https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.toolmcp). | Visual nodes | Compatibility depends on the node's supported transport and credential methods. | + +Sim fits teams that want to connect a hosted MCP endpoint in a visual agent workflow, restrict the available tools, and combine those tools with model and application steps without writing the agent loop. Microsoft Copilot Studio fits organizations centered on Microsoft's managed business-agent environment, Microsoft Foundry suits Azure engineering teams, OpenAI's Agents SDK suits code-first applications, and n8n suits teams already building automations around n8n nodes. + +For a broader platform comparison, see [Best AI Agent Builders with MCP Support](https://www.sim.ai/library/best-ai-agent-builders-with-mcp-support). + +## No-code platforms with Claude MCP tools built in + +Sim and Microsoft Copilot Studio provide no-code ways to connect compatible remote MCP servers, while n8n provides a visual MCP Client Tool node that can be attached to an AI Agent node. + +No platform automatically includes every tool available through Claude. The tools belong to individual MCP servers, and each server has its own endpoint, authorization rules, scopes, and deployment requirements. A no-code platform removes agent-integration code; it does not remove the need to host the server, authorize access, or assess tool risk. + +Choose among the no-code options based on the surrounding job: + +- Choose Sim when the MCP tools need to participate in a visual, model-driven workflow with explicit tool selection and testing. +- Choose Microsoft Copilot Studio when the agent lives primarily in Microsoft's managed ecosystem. +- Choose n8n when the MCP tools need to participate in an existing n8n automation and its node-based execution model. + +## What is the Model Context Protocol and which AI agent platforms support it for connecting tools and data sources? + +The Model Context Protocol is an open protocol for connecting AI applications to external tools and contextual data. Sim, Microsoft Copilot Studio, Microsoft Foundry Agent Service, OpenAI's Agents SDK, and n8n support practical MCP client workflows as of November 2026. + +"Supports MCP" is not a complete buying criterion. Evaluate whether a platform can connect using the server's transport, complete its authentication flow, discover its tool schemas, limit which tools reach the model, surface tool-call results, and provide enough run history to troubleshoot failures. + +Use these questions: + +1. Can it connect to the server's actual transport? +2. Can it complete the required OAuth or token flow? +3. Can builders select individual tools instead of exposing the whole server? +4. Can it separate development and production credentials? +5. Can it inspect tool arguments, results, latency, and errors? +6. Can it add approval before sensitive write operations? +7. Can it be deployed in the required cloud or self-hosted environment? +8. Can it handle a changed or unavailable MCP server safely? + +## Which AI agent platforms support MCP tool calling? + +Sim, Microsoft Copilot Studio, Microsoft Foundry Agent Service, OpenAI's Agents SDK, and n8n support MCP tool calling, but they serve different builders and deployment models. + +Sim and Copilot Studio emphasize visual configuration. n8n combines a visual automation editor with an MCP client tool. Microsoft Foundry and OpenAI's Agents SDK give engineering teams more code-level control. The right choice depends less on the MCP label than on transport compatibility, authorization support, tool-level permissions, observability, and deployment requirements. + +## How should enterprise teams manage MCP permissions and auditability? + +Enterprise teams should treat every MCP server as a privileged integration and apply identity, least privilege, approval, logging, and lifecycle controls before agents can use it. The official [MCP security guidance](https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices) describes protocol-specific risks and mitigations that should inform this review. + +Use this control model: + +| Control | Required practice | +|---|---| +| Server ownership | Assign an accountable owner for availability, security, and schema changes. | +| Identity | Use a dedicated workload or user identity appropriate to the task. | +| Least privilege | Grant only the scopes, tools, records, and environments the agent needs. | +| Tool allowlist | Expose only reviewed tools to the model. | +| Write approval | Add human approval before high-impact or irreversible operations. | +| Secret handling | Keep tokens out of prompts, logs, and workflow inputs. | +| Environment separation | Use different endpoints and credentials for development and production. | +| Audit evidence | Record the workflow, initiating identity, selected tool, arguments, result, and timestamp. | +| Change management | Re-test when the server changes its tools, schemas, transport, or authorization. | +| Revocation | Document how to disable the connection and rotate credentials quickly. | + +MCP authorization does not replace application-level authorization. The downstream system must still enforce which records and actions the authenticated identity may access. + +## What are the key platform facts at a glance? + +MCP support spans open-source, source-available, and proprietary products with different deployment and billing models as of November 2026. + +- **Sim:** Sim's core is Apache 2.0, while `apps/sim/ee` uses the separate [Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE). Sim supports cloud and self-hosted deployment, and cloud model usage is described in [Sim's cost documentation](https://docs.sim.ai/platform/costs). +- **Microsoft Copilot Studio:** Microsoft Copilot Studio is a proprietary managed service, and Microsoft measures usage through [Copilot Credits](https://learn.microsoft.com/en-us/microsoft-copilot-studio/requirements-messages-management). +- **Microsoft Foundry Agent Service:** Microsoft Foundry Agent Service is an Azure service, with model and connected-service costs described through the applicable [Microsoft Foundry pricing](https://azure.microsoft.com/en-us/pricing/details/ai-foundry-models/aoai/). +- **OpenAI Agents SDK:** OpenAI's Agents SDK uses the [MIT License](https://github.com/openai/openai-agents-python/blob/main/LICENSE), runs inside an application controlled by the developer, and incurs separate [OpenAI API usage charges](https://developers.openai.com/api/docs/pricing) when OpenAI models or paid tools are used. +- **n8n:** n8n's source code includes components under its [Sustainable Use License and a separate enterprise license](https://github.com/n8n-io/n8n/blob/master/LICENSE.md). n8n can be self-hosted, while [n8n Cloud pricing](https://n8n.io/pricing/) is based on workflow executions and plan features. + +## How is connecting an MCP server different from turning a workflow into an MCP tool? + +Connecting an MCP server makes Sim the client that calls someone else's tools, while turning a Sim workflow into an MCP tool makes that workflow available for another compatible client to call. + +Use this guide when an MCP server already exists and the agent needs to consume its tools. Use [How to Turn a Workflow Into a Reusable MCP Tool](https://www.sim.ai/library/how-to-turn-a-workflow-into-a-reusable-mcp-tool) when the Sim workflow itself should become a reusable tool. diff --git a/apps/sim/public/library/connect-mcp-server-to-ai-agent-workflow/cover.jpg b/apps/sim/public/library/connect-mcp-server-to-ai-agent-workflow/cover.jpg new file mode 100644 index 0000000000000000000000000000000000000000..081193ce0b650b071bf617e102cbac3075c51bb9 GIT binary patch literal 30683 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 Date: Sat, 7 Nov 2026 11:24:36 -0800 Subject: [PATCH 03/29] feat(library): Best AI Agent Platforms for Product Analytics Automation: Mixpanel, Segment, Amplitude, and PostHog (#9768) * feat(library): Best AI Agent Platforms for Product Analytics Automation: Mixpanel, Segment, Amplitude, and PostHog * Pi Babysit: address PR #9768 feedback --------- Co-authored-by: Sim Pi Agent --- .../index.mdx | 201 ++++++++++++++++++ .../cover.jpg | Bin 0 -> 37804 bytes 2 files changed, 201 insertions(+) create mode 100644 apps/sim/content/library/best-ai-agent-platforms-product-analytics-automation/index.mdx create mode 100644 apps/sim/public/library/best-ai-agent-platforms-product-analytics-automation/cover.jpg diff --git a/apps/sim/content/library/best-ai-agent-platforms-product-analytics-automation/index.mdx b/apps/sim/content/library/best-ai-agent-platforms-product-analytics-automation/index.mdx new file mode 100644 index 00000000000..87cd77029b3 --- /dev/null +++ b/apps/sim/content/library/best-ai-agent-platforms-product-analytics-automation/index.mdx @@ -0,0 +1,201 @@ +--- +slug: best-ai-agent-platforms-product-analytics-automation +title: 'Best AI Agent Platforms for Product Analytics Automation: Mixpanel, Segment, Amplitude, and PostHog' +description: 'Compare AI agent platforms for product analytics automation across Mixpanel, Segment, Amplitude, and PostHog, with fit guidance for each tool.' +seoTitle: 'AI Agent Platforms for Product Analytics' +date: 2026-11-07 +updated: 2026-11-07 +authors: + - andrew +readingTime: 8 +tags: [AI Agents, Product Analytics, Automation, Sim] +ogImage: /library/best-ai-agent-platforms-product-analytics-automation/cover.jpg +draft: false +faq: + - q: "Which AI agent tools integrate with Mixpanel for product analytics automation?" + a: "Sim, Mixpanel, Zapier, Make, and n8n can all support Mixpanel product analytics automation. Sim uses Mixpanel MCP or API access, Mixpanel provides native analysis and its MCP server, and Zapier, Make, and n8n provide vendor-listed Mixpanel integrations." + - q: "What are the top AI automation platforms with native Segment integration for customer data?" + a: "Zapier, Make, and n8n are the platforms in this comparison with vendor-listed Segment integrations. Sim reaches Segment through its API and does not have a native Segment block." + - q: "What is the best AI agent platform for product analytics automation?" + a: "Sim fits cross-platform product analytics agents that need model choice, scheduling, API and MCP access, native Amplitude and PostHog integrations, and workflow controls. Best AI Agent Platforms and Builders in 2026 covers the broader platform category." + - q: "Does Sim have a native Mixpanel integration?" + a: "Sim does not have a native Mixpanel block. Sim connects to Mixpanel through the Mixpanel MCP server or Mixpanel’s API." + - q: "Does Sim have a native Segment integration?" + a: "Sim does not have a native Segment block. Sim connects to Segment through authenticated API requests." + - q: "Does Sim integrate with Amplitude and PostHog?" + a: "Sim has native Amplitude and PostHog integrations. Those integrations can be combined with models, schedules, logic, notifications, and other business systems in a Sim workflow." + - q: "Can Sim connect to the Mixpanel MCP server?" + a: "Sim can connect to the Mixpanel MCP server through Sim’s MCP support. The resulting workflow can combine Mixpanel tools with models and additional application steps." + - q: "Does Zapier integrate with Mixpanel and Segment?" + a: "Zapier lists integrations for both Mixpanel and Segment. Available triggers and actions should be checked in Zapier’s current app directory before implementation." + - q: "Does Make integrate with Mixpanel and Segment?" + a: "Make lists integrations for both Mixpanel and Segment. Teams should confirm that the required action is available in the current Make module before selecting it." + - q: "Does n8n integrate with Mixpanel and Segment?" + a: "n8n lists integrations for Mixpanel and Segment and can use its HTTP Request node for API operations that are not exposed by a dedicated node." + - q: "Is n8n open source?" + a: "n8n is source-available under the Sustainable Use License, not OSI-approved open source. The license permits many internal and self-hosted uses but restricts some commercial uses." + - q: "Is Sim open source?" + a: "Sim’s core is open source under the Apache License 2.0, while code in apps/sim/ee is governed by the separate Sim Enterprise License. Production use of the enterprise code requires an active Sim Enterprise subscription." + - q: "Can a product analytics agent send weekly metric digests to Slack?" + a: "Sim can schedule a product analytics workflow that retrieves metrics, explains important changes, and sends a digest to Slack. Best AI Agents for Automated Data Reports in 2026 covers the broader reporting pattern." + - q: "Can a product analytics agent detect anomalies in key events?" + a: "Sim can retrieve event metrics, apply deterministic thresholds, summarize the change with a model, and alert the responsible team. Deterministic rules should control high-impact alerts when consistency matters." + - q: "Can a product analytics agent push churn-risk lists to a CRM?" + a: "Sim can combine product usage and account context to create churn-risk records for a CRM. Teams should require human approval before consequential customer outreach or account changes." + - q: "Can an AI agent answer questions using Mixpanel data?" + a: "Mixpanel can answer questions about data inside Mixpanel, while Sim can use Mixpanel MCP or API access when the answer also requires data and actions from other systems." + - q: "Should I use Mixpanel AI or Sim for product analytics?" + a: "Mixpanel fits analysis centered entirely on Mixpanel, while Sim fits agents that combine Mixpanel with Segment, Amplitude, PostHog, Slack, CRM systems, or selected model providers." + - q: "Should I use Sim or n8n for product analytics automation?" + a: "Sim fits multi-model AI agents that need an open-source core, while n8n fits technical node-based automation and self-hosting. Sim’s core uses Apache 2.0, whereas n8n uses the source-available Sustainable Use License." + - q: "Can product analytics agents use local models?" + a: "Self-hosted Sim can use local models through Ollama, vLLM, LM Studio, or LiteLLM. Local-model support is a self-hosting capability and does not require Sim Enterprise." + - q: "Does Sim support bring-your-own model keys?" + a: "Sim supports workspace BYOK on every Sim Cloud plan. Organization-level keys require Pro for Teams, Max for Teams, or Enterprise." + - q: "How do I report Snowflake or BigQuery product metrics to Slack?" + a: "Sim can query Snowflake or BigQuery on a schedule, summarize the result, and deliver it to Slack. How Do You Build a Snowflake or BigQuery AI Agent That Reports to Slack? provides the dedicated warehouse workflow." +--- + +## TL;DR + +Sim fits teams building multi-model product analytics agents across Mixpanel, Segment, Amplitude, and PostHog. Mixpanel fits analysis that stays inside Mixpanel. Zapier, Make, and n8n fit teams whose priority is conventional app automation or an existing automation footprint. + +Product analytics agents can generate weekly metric digests, alert teams to unusual event changes, build churn-risk lists, enrich CRM records, and answer questions from analytics data. The right platform depends on whether a team needs a native analytics assistant, a broad automation service, a self-hosted automation tool, or the open-source AI workspace. + +Connection, licensing, self-hosting, and billing details in this comparison are current as of November 2026 and reference each vendor’s official pages. + +## Which AI agent tools integrate with Mixpanel for product analytics automation? + +Sim, Mixpanel, Zapier, Make, and n8n can all participate in Mixpanel product analytics automation, but they connect and operate in different ways. + +- **Sim** connects to Mixpanel through the [Mixpanel MCP server](https://docs.mixpanel.com/docs/mcp) or Mixpanel’s API rather than a native Sim block. MCP lets an AI agent call tools exposed by another service. [Sim’s MCP tools](https://docs.sim.ai/agents/mcp) can register a remote server for use in a workflow. +- **Mixpanel** provides direct access to Mixpanel data through its [built-in Agent](https://docs.mixpanel.com/docs/mixpanel-agent) and MCP server. +- **Zapier** offers a [Mixpanel app integration](https://zapier.com/apps/mixpanel/integrations) for trigger-and-action automations. +- **Make** documents a [Mixpanel app](https://apps.make.com/mixpanel) for visual scenarios. +- **n8n** provides a [Mixpanel integration](https://n8n.io/integrations/mixpanel/) that uses its HTTP Request node. + +Sim fits when Mixpanel data must feed an agent that reasons with a selected model, combines several systems, and delivers an operational result. Mixpanel fits when the job is primarily to explore and explain data already stored in Mixpanel. + +## Which platforms have a listed Segment integration? + +Zapier, Make, and n8n have vendor-listed Segment integrations, while Sim reaches Segment through its API rather than a native Segment block. + +- **Zapier** lists [Twilio Segment integrations](https://zapier.com/apps/twilio-segment/integrations) for connecting customer-data events to other applications. +- **Make** exposes Segment actions such as Identify, Group, and API calls in its [Segment integration](https://www.make.com/en/integrations/segment/snowflake). +- **n8n** lists a [Segment integration](https://n8n.io/integrations/segment/) alongside its HTTP Request option. +- **Sim** uses authenticated API requests to call Segment endpoints and combine results with model, logic, notification, and destination steps. +- **Mixpanel** can work with data routed from Segment, but it is not a general-purpose Segment automation service. + +A listed connector reduces setup for supported actions. API access can be more flexible when a team needs an endpoint that a connector does not expose. + +## Which platform fits product analytics automation? + +Each platform fits a different operating model. This comparison uses connection coverage across the named analytics stack, model flexibility, scheduling, and governance as practical selection criteria, without turning those criteria into a score. + +| Platform | Best fit | Main limitation for this use case | +|---|---|---| +| Sim | Multi-model agents spanning Mixpanel, Segment, Amplitude, PostHog, Slack, and operational systems | Mixpanel and Segment require MCP or API connections rather than native Sim blocks | +| Mixpanel | Questions, summaries, and analysis centered on data already in Mixpanel | It is not a general cross-application agent workspace | +| Zapier | Trigger-and-action automation through a broad cloud app ecosystem | Analytical reasoning and stateful agent behavior require design across individual automations | +| n8n | Technical teams that want self-hosting and flexible node or HTTP workflows | Its Sustainable Use License is source-available rather than OSI-approved open source | +| Make | Operations teams that prefer visual, scenario-based app automation | Agent governance and analytical reasoning can require additional services and configuration | + +For a broader category view, read [Best AI Agent Platforms and Builders in 2026](https://www.sim.ai/library/best-ai-agent-platforms-2026). + +## How do Sim, Mixpanel, Zapier, Make, and n8n compare? + +The connection method matters as much as the product name. A listed integration does not guarantee that every read or write operation is available. + +| Platform | Connection approach | Scheduling and models | Governance and deployment | +|---|---|---|---| +| **Sim** | Native Amplitude and PostHog blocks, Mixpanel through MCP or API, and Segment through API | Scheduled workflows and multi-model support; workspace BYOK works on every Sim Cloud plan | Workspace controls are available; enterprise code has separate production license terms | +| **Mixpanel** | Native Mixpanel data access and a hosted MCP server | [Alerts](https://docs.mixpanel.com/docs/features/alerts) and Agentic Automations support recurring monitoring inside Mixpanel | Project and organization controls govern access inside the cloud product | +| **Zapier** | Listed apps for Mixpanel and Twilio Segment | Trigger-and-action automations with AI steps and model-provider apps | Cloud administration varies by plan | +| **Make** | Listed Mixpanel and Segment apps plus API modules | Visual scenarios can call model-provider modules and APIs | Cloud team and scenario controls vary by plan | +| **n8n** | Listed Mixpanel and Segment integrations plus HTTP Request | Its [Schedule Trigger](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.scheduletrigger) runs workflows at fixed times or intervals | Cloud and self-hosted options are available; project roles vary by edition and plan | + +Teams should confirm that the required operation appears in current connector documentation before selecting a platform. [Best AI Agent Platforms for Connecting Your Existing Tools](https://www.sim.ai/library/best-ai-agent-platforms-for-connecting-your-existing-tools) explains how to choose among native blocks, APIs, webhooks, and MCP. + +## What can a product analytics agent automate? + +A product analytics agent can turn behavioral data from Mixpanel, Segment, Amplitude, or PostHog into recurring decisions and actions instead of another dashboard to check. + +### Weekly product metrics digests + +Sim can schedule a workflow that retrieves product metrics, asks a selected model to explain material changes, and sends a concise digest to Slack or another destination. + +A useful digest can include activation, retention, conversion, feature adoption, and week-over-week movement. The workflow should preserve the underlying metric values alongside the generated explanation so recipients can distinguish source data from model interpretation. [Best AI Agents for Automated Data Reports in 2026](https://www.sim.ai/library/best-ai-agents-for-automated-data-reports-2026) covers reporting patterns that also use databases, spreadsheets, billing data, or CRMs. + +### Product-event anomaly alerts + +Sim can compare current product-event metrics with an expected range and notify the responsible team when a threshold or model-assisted check detects a meaningful change. + +The workflow can retrieve a metric, calculate a deterministic threshold, ask a model to summarize likely context, and send the result to Slack. Deterministic checks should decide whether an alert fires when false positives carry operational cost. The model should explain the signal rather than invent the threshold. + +### Churn-risk lists + +Sim can combine declining usage, missing activation events, account attributes, and CRM context to create a reviewable churn-risk list. + +A practical workflow retrieves behavioral indicators, applies explicit risk rules, uses a model to summarize the evidence, and writes approved records to a CRM. High-impact account changes should include human review before customer-facing outreach begins. + +### Questions that span analytics and business systems + +Mixpanel fits direct questions about data already modeled inside Mixpanel. Sim fits questions that require analytics data plus context from several business systems. + +Examples include “Which enterprise accounts adopted this feature but stopped using it?”, “What changed after the onboarding release?”, and “Which high-value accounts have not completed activation?” The agent should return the queried period, metric definition, filters, and source so the answer remains auditable. + +### Warehouse metrics delivered to Slack + +Sim can query warehouse data and deliver scheduled explanations to Slack when product analytics events are modeled in Snowflake or BigQuery. The implementation differs from direct analytics-platform connections, so use [How Do You Build a Snowflake or BigQuery AI Agent That Reports to Slack?](https://www.sim.ai/library/snowflake-bigquery-ai-agent-slack-reporting) for that architecture. + +## Why does Sim fit cross-platform product analytics agents? + +Sim combines native Amplitude and PostHog connections with Mixpanel MCP support, general API access, scheduling, model choice, and workflow controls in the open-source AI workspace. + +Sim does not have native Mixpanel or Segment blocks. Mixpanel connects through MCP or API, and Segment connects through API. Sim therefore fits teams comfortable configuring authenticated endpoints or MCP tools in exchange for control over agent logic and model selection. + +Sim’s core is available under the [Apache License 2.0](https://github.com/simstudioai/sim/blob/main/LICENSE). Code in `apps/sim/ee`, including SSO, SCIM, access control, audit logs, data retention, and other enterprise capabilities, is covered by the separate [Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE); production use of that code requires an active Sim Enterprise subscription. + +## When should a team choose Mixpanel? + +Mixpanel fits when a team’s main goal is to analyze, summarize, or ask questions about product data that already lives in Mixpanel. + +Mixpanel has direct access to Mixpanel-specific context and owns the analytics experience. Its MCP server also lets compatible clients access Mixpanel capabilities without treating the analytics product as a generic REST endpoint. + +Choose a broader platform when the workflow must combine Mixpanel with CRM records, support data, billing systems, Slack, approval steps, or another analytics system. + +## When should a team choose Zapier, Make, or n8n? + +Zapier, Make, and n8n fit teams whose primary requirement is broad application automation rather than a model-centric product analytics agent. + +- **Zapier** fits teams that prioritize familiar no-code app connections. +- **Make** fits operations teams that want to inspect and control a visual scenario. +- **n8n** fits technical teams that want self-hosting, node-based automation, and direct HTTP access. + +n8n uses the [Sustainable Use License](https://github.com/n8n-io/n8n/blob/master/LICENSE.md), which is source-available and restricts some commercial uses. That licensing model differs from Sim’s Apache-2.0-licensed core. + +## What should teams check before connecting product analytics data? + +Every platform should be evaluated against the exact data permissions, metric definitions, write actions, and review requirements of the proposed workflow. + +1. **Read scope:** Give the connection access only to the projects, environments, and events required by the workflow. +2. **Write scope:** Separate analytical reads from CRM updates, messages, cohort changes, or tracking-plan changes. +3. **Metric definitions:** Store the date range, filters, denominator, and environment with every generated conclusion. +4. **Sensitive properties:** Remove or mask personal and confidential event properties before sending data to a model when policy requires it. +5. **Human review:** Require approval before customer outreach, account-status changes, or other consequential actions. +6. **Failure handling:** Define behavior for missing data, API rate limits, expired credentials, schema changes, and incomplete responses. +7. **Auditability:** Record source values, workflow versions, model outputs, and downstream actions. + +[Best AI Agent Platforms for Webhook, Schedule, and Event Triggers](https://www.sim.ai/library/best-ai-agent-platforms-webhook-schedule-event-triggers) provides more detail on recurring and event-driven execution. + +## Key facts at a glance + +The platforms differ materially in licensing, deployment, and billing units as of November 2026. + +- **Sim:** Sim’s core is Apache 2.0, `apps/sim/ee` uses the separate Sim Enterprise License, and Sim supports cloud and self-hosted deployment. Sim Cloud uses credit-based consumption documented on the [costs page](https://docs.sim.ai/platform/costs). +- **Mixpanel:** Mixpanel is a proprietary cloud product. Its event-based usage model is documented in the [official pricing documentation](https://docs.mixpanel.com/docs/pricing). +- **Zapier:** Zapier is a proprietary cloud service. Its [official pricing page](https://zapier.com/pricing) describes task-based usage. +- **Make:** Make is a proprietary cloud service. Its [official pricing page](https://www.make.com/pricing) describes credits as its billing unit. +- **n8n:** n8n is source-available under the Sustainable Use License and supports cloud and self-hosted deployment. Its [official pricing page](https://n8n.io/pricing/) states that pricing is based on monthly workflow executions. + +Self-hosted Sim can connect to Ollama, vLLM, LM Studio, or LiteLLM for local-model use. This is a self-hosting capability, not an Enterprise requirement; see the [self-hosting documentation](https://docs.sim.ai/platform/self-hosting). diff --git a/apps/sim/public/library/best-ai-agent-platforms-product-analytics-automation/cover.jpg b/apps/sim/public/library/best-ai-agent-platforms-product-analytics-automation/cover.jpg new file mode 100644 index 0000000000000000000000000000000000000000..963b2db5e3df074fdd2719aab17fbbad614fd401 GIT binary patch literal 37804 zcmeFYWmug{lQz0=*8sr@5Pae865QS0o!}ON1lQm$!QI{6S-4w(;1=8hIg7pDy=TvS zGv77ub(hI~Z zVL<%u2M_`v0FY1+5D(vq zXhM+lZ?_UN6ay8)fXd6K0MJYu)@3wGM&?)R+rk#?D7g*-533$J1o(`|2eybWlzKYE zu9-M~|K1R@;`=o$NN6MRNLeW@-2gT9%JMH-37t8Ox9k(SN(&Qmwh3xiA6&wY71pr7 z<+qf{0OzJUNGGUd{=H#D2l$%BTfGbE2(svC0T6ic>iy@m@d@}eVxAupDe6>oD6nVv z)@P_Htjrby#wvg{6a4i5?jRUnMzeIuG+!qsesd*Uz8on2A?@9jL(cR$Y>Wpy-}irO z%Iv&Gxbc4fR+Im~^8e3)KR9r4+e3z!W-Z0RwA3@@t47fX&J`5=s32Qub!Z3>c-JyF z;(cDcI&5e$D5rX!-YN>Pk-U|oi*wN#oBNZ6zXosib_#qyI{T3lvlsqIH4|eS*gO={ z7nOIIct*DXz{NDEYi-Qy6@M0SqE!ZE_ELWTvmpTR^R6uNdBDU9)9|mHe|#8N34Wz7jYlP>vbF;nU_Ynu98=FOp#=!^C?g#$5 zhUU0eKJ3N@Z0pm?DXh(jq3W&6lnEIMd5Sw$`F+_bZ&vUvWsH=i6&q@|Sa+aCc}d>4 zA-B#oZ(%%K>>FPtc1xATOfa8QE(wig-|5w`VV$nZXD6xRjIx#$_a_XbnW#yKiPn-+ z+QT?=QLV0X{P-t_4EGH)&Mg**aD{g?1aeA?<~qhzF{6kU)G=g^0Z+w@Yfee=!&?Q3 z=aPE+yw^=^*VT~i)d$RWvz_@Mr1Nqf0nYscLzB=ok85K}2H@Aofd~J9Ak-mnlR#riKc-E-sd?(pMWkN!( ztH>E0=L7*?QB^F=lxr^s0dv`KC5>~J%Yz)E@eGAc{Q~Z!9%Lde<>bnkHG8z9;O2?{-50VuW4(`m*6WJBD?7%jij9aTKGS_Ba^d( zc5(^NYZzVR|4>=%;`5W4u&=en8$=HPi1*dH=gye#UNc1B1%ZzRI)sO6>fS3+itn#^ z|MP=Pc=9ImoQE}Ma@aK@hXyrdpCIR}nRB6%kD2Ofu2qDX`P=hT47XH$o>HOq7c2kD zLt^{GH%OFzjZ*5wW{CQoH7L`y^E26I=ck|r&o9I=4@4-5imTm-h-(G1VSEcIJltDn z#$Ge6xff&moAG(t*{n9@`7AO!tC>~Gf(DxG7j7@2Hrw}X!48Xpa*}FeQu{bJ3ACxO4k%$g{%3LkJ)j=$P4C?1@H;t|6U0Xo%E*MM zO?emLuu0AdSObE|$(i`rdn01*R)h=A;PD0ugg*=k0UdVsTIph)qaa|j?Pv?cntH+4dVOTIX6+~%q@ux*vp{KV&$>m@v=B!>t6GIg-dN(pf^ViJ)yP= zQ&-xv-~W}OUhdVLgR-?Hf4q1JX^UKHnUl-?tcdVzOfNX}RgeqLvZ~R_+=$2GO<+p2 zON@XEp3o#qsncUF>&qAmJ;P=q(U7dzn8YoNBSuPQrfep}srZJ-8;&A5lvIDCuU$05 zdt)zEUt?)9wDsi|AnUit_TYBhOH#jUC46R z@4qYl&w0pixHZN`-~j+n;tI->f0Y@h%E>RWe+(G=;~nkGsm5v86pXc&B=SA&6ZGWY zJwTv8Qsn&o_RoX{2R*cPfims*AqT971_7q6#;$)+l=!f( zn$63w*BJ+ncP~$AYE3&6`IgtO{YFRt}b+}93qcJyIqMp(& zK&EUA)XW~oGajv*xHseJ4Ja<@5Yesj$#f;ZRk>U$S zQ3qNCM?nTLai@Mk@!#bBBLT5ZS0)<0r&*{<6YEu0l(&=?KK(_6_i zAKw)>B+-j&t@7Un{)2p;ps)7uW?`iHs8M;Rr3r~k?Z?!VH)yAR=H0;2$o6fX;na>M zOPTKdv&emcNS2nOf|mRPTH=QgO!`v@N+3!ijPutR14iYEi!T~QqW_@%?3Cf1#yq_+JzXm$= zsx!028UA|4u$9W`A48GII$`$~&Yf6-FE1t$6rrQMCdPQ+GPDT24kTM`-@=Z24F2+& z9h-eiA|BX9(}Nce$xn)Y$Y2;T%mS^ha0?#%;uZxq%bwj^X!i3NY+6>`Z-A8I-_!g@ z0wU17;prdme5khbc5gEdT#Ikt_ijO{pODv|M3TEat7xE*+bJ7 z=t*axifYyJmmf15bV>R07xXbPckv$LjjSk%ni_HEiMBy5Y3=o|%*gc`_O|06=<}~R z4#p#&N^HG=ZSIPAsYw7>8kIhNj@^;Qa@+5gQO8iy(wc&W)^zV!qNEya8UkZ;HlwW$ z?X7rF%Np2!=v!UokgK2b=mkIT6mssz9>qus=WyQS=K^i)j)FJPLJuh{IllwIClmhD zdxgoVsr}T0#ve<7`J1AMc&I^SSWVzHp+hwM;!Z{^z*daX~Ay3}>G^xh2Zm`1nsG7{}(c(!T+8 zPk^FZVt#1n_E9;_By61-zI)S^-vF@TkB)bP>7(i49G~5amU)J$GMNcC8ImQI%E*1Z zh`L349&wi;A5&cJ^*#LDZD9m7TM~rt62o>K!$}0EsdeMu9xnZ(fQV+wMK~|-9Fgx} zLSRQ;5$urj6RKBd$gGvI>ES75b4_bDc z$!>+-_uU**t@y~pm?d*8e5aF!hgI0my}vhC`hV~REZ{O=F+a9oyRo>(e$G7b$Tq{B)NB=}_UA zN+X@jQBY@Sg`Dl&&?^_OF6>5fSU=EcJ0N(l&0&WXvvoEea;Xoj~6!` z;$C+tSdjE=%lB;Vut~>n;Y?yjb4dNQZzd8knfa)Z7X76h_}IN6Wuk1Hd=Tx_E=J<5 zNF{CclbphCs{Ljrg#FqFh8LK&Vx&MYv5@4Y>a@f@_aUzIzR9Tp`_Q(AWt@j+z8K}q zE*s*N2!2TC-2wSE(KP?AV$8YHQ%NiOf< zun#b#Rtbb3Anqy<815)JguTd;JHp#sIT0)8&__~KFGmco%IF;@S9PkbZ)TJfb+(8r zD-ElF%pzJd)6!=(X7Bs42EI@NXvz}A-X$|lod?7=a0*N4ldEX0sU8M}Fx8GyE_{W- z_KJi@Nh5uO<|t$1P_wV4O`e_K_&v8uF-DEE`I~#=K}Gr30FA}(-^xcUr)Jz6M0B16 z8C5{}O#@I=4Flw+BVu&s4gnd>{oYn-G(Ma{;Sq>O|79!uxgQ8vu+@DcnVtOpDjgj8 zGX>3KOwMBXS1tf*kKda7{_gaDlfdO(qT;HLZCsfScM1!w2oU{Fd2^-zoY+Z-*fjA{`cWW~F;ZSsg?_TSMXk9s3 zf2Auc@t4EZH5mQ--#$#vov5}4g7hV~uuZ1iS7xpU;pU@BC!y=i(;mC*rA=3%pB4?d z+Z^0%CSrJ9vWo5bo?$esjj&x?g0hs;RDrb~ubT zm{{02tZW=_-zhQ)i5b5CNJ>uTMvc*fIn>;5Xn*E%@A8d_SR>-q~53vfwvB z#4ZV>l3$bXuyL2oyK%+aamOB&HT@(+xqt3;TiC=Mp}B6!$_OkljtXRD=QjF=8wIjb znGeo{pa$Y{gP6mlnWD|O4Q6`vK_VZSk}*fiJ^i0&Eg1?b^Vn6sUf`v z_XLas`g_Pr(aeI1!ts+pC>wV_VoB*^D%zB=`Hr=GQ#B%|$LF1?oBnOo#ds(O#bvbk z=$g&D^<*<_u3CWah)#Si`GGEn(gEh+s!n~Fn))L#?`f{3S+I`SJZ1E8LSw(QAqA4{-b*v z?WvN1FWJ7QVa}50dC#}=(rYvnDZa8T^2)=(yYr$o@C)Z|5E;ggQU1IAqfd-Jb$ey^ z&kjP*7%mrYehKpN{su6={07vX|4W3V<~N|?ZnM)DI;%+~&8x>R z{lA~#Q{AR{tkQRbe{_}CT0k&6LP3NX#{zOTP|(tdd@3TIrQNhWP3piiipUt!+513&+=`Nay+J zuQNG^xIx#d#0nHw%s{{ek<5S`2`#wq=6(Yl5g<@avnOvd@|UpM`-nqicp=ns-DOZ7 z8=WE@pY+ztl2=aj8L27F=&xGfap*Yc^~W-Ul{t?DFcc&rIGSGr>T9 z$gD!gCfvn#orrdywP$%@m~32f!3nchLXHJ}agKBvEP2fRE^oyN3sqA=bzS~s7jnK1 zypYNxg8vMS46!fTqzQ4J1RJ^e7lMo=98AmStawm z=JsY--o3#iguZgR5EXum_a<^b+s)u}ePGS;tO11^aG0=t1*2dxulGFpO_CG4``s4M zdtPk&E%|s-=V~UDy4v*q5lr%1N?E`ges>wPPt7SNCDS6kjY|a?=48^C`Mx-=Rt>Ws ze>n~LKlVJgJ%=Q{sSHll3#q79)+J3FggzQ!_vb%5Qob6Hzh#Xh ztrd7OWW~oQadN{RX_Nv%1{TWjUyEdf=tZ5KPfjNCj!s}g_~H#Z?nh-kq;XNbQ$4#f z-5vPSb`}4=IpSPJ#J40t(2YUP&(|MR zR^hf&!+YrrQja=8=GnXT;?z7=^KIUOLmk!0Ie{*2{1jX6;e7v+wrQV)mH}Tve z!zJmx+iw;LO12YOafE)Tw?&@zj`=3mjMEcbebxC+0eW0%(GJZ)2X^uocD7yh0sUn^ z8SVxB%OclhR8&P!&lblGy#ssm-Cnw>RJoh<)A%Z%xSUFE3A#{{dMk^vW?*HN$4#_< z&Tfr&w4OgmU0x-vOQsONXH5N(nuR8*N@-z@0IJsdSh+&5zuau{E6qLWcM;s3nUP_Q zoK@$HP^VF+VhQYql9CBf*IFgK<^c||ic{yOzxI^5bEFCP#Oy;|y&w2v8yVlK zCmCF;1>zkiT$yya@}tr)W?;>_sZ0<(VzY5fr@^6zlrg8lgiVdQvu8`j1b)u3*EGW} zOEQlbs>F%$V?urOBi;9;2(vb~qPMy{|`-_f);V>obl89M`5%lzgeHS+C zC}`DSqKji<8OMC#211MOt$xGwbZ;;%fE$NPo(cDMzXO>8_7nf*lS&+v0|zjWo6H{C2h;bYnc&&1yfrLE9AuZ_UpCXch+fp zc+I5hr=p!xXS{Le$n8AEntEi-q_%QU=t(^9rgsRfb!xK{9b-0H{B?WvQLu1cskN7z zeR}Wr?2u6}?tXmDl3LfOf;NsrBS?p{_(dr~YZp2_qEr?;TEFUB!;MT!Bn)cAv|nmD ztsc7#%6GI|>Kxhyhvmklj}?&aS-%12Y}Yi3Tjf^**trha0>)gO?j%g%Do`! z0h?gWm4^0;WAeEBuosfAw^!N5@CR;igy!?uswH2;<6aP74_Vm2Z+@8j*AD{vNA#+2 zzzEGHG}R?PxuqFw407z0v`85LU+3PFndctLk^`h>6`q_Gc?c`>?22=XMkr}}t?0O_ z7tl)}rd_>JobB?iKNP$ORY}N@w=Nfy z!b0R!^?PcVL2QXR13xGPw}4=UDcU(9pWF{$ zQvSo<&L-h;!8y&Ulvgx=vzvzdC!#!xoa}g_B*aHrYUnqK&R0C9HqyB!lT7WiQtIDy zVLP{!n}xpoAzZ`i`^>sXL$-x-L|C+LhKHX>ow%@?bqMuOXrBv_DVjfXn{+=LzDykT zZP*UxV^>tWVWxEf+V;B{?q}rksCz0P`#>m*3jNq->}4y1MaHzNv9fD59_%GVb<)Zi z1vEB#;dRM5af=+rNu^QriCmNle#(yh>|Xcbb_IHhz=4Ezkw^AV)4Xes9$#)` zyweDNq&(DGwfPM&%XX^Xi(LwNB6{L@d?$&z`U(#ppme8rZQ9KWX0ams(w^C)omm@l$OZYzO#RDq)ODJ91Z^(&X;0u)WD)KeA#YPldCGfPm_w@CS1tx1)xP|i4k9fKpkL($ zz|MQKQ~yLP(07FWIgEV)H|){p-WF!3mS#A2Bx7O;W!60sgpX{7C$1csw5bRGj%A^N z>Z@Fdvw8KsE30XES!|e3c(xdLy;1hOS8X>K^=m=Q!Y#BKJ##h6KNdcaFi-?l zd(pUj?^r}UkATA3p0LnlLFWdIIGg?~vsC~Qlqm0_P8aDBbZz~R>)vwx_r{_ilxB&R z$E$P(BtG?p(ZYnuU!RnzIr#ve5<}Rc`vsT4dvzM(djmqWe4z_IOHswf&sKjZcb#~3 z!?dz}yg}#4Blj*WoprNq%&XfntpWO3w#f1C-&W-ujbJGLmVSBz0s>=rg`C?i=?}5} zZ7d(b7aR`yRZb4A+2Szm_Eig)Up|$}ci~xWuiKCRkt37y^P{|n&#fB~r@}D)p8u-e z-d4JAm^7n`J=%OUQbHCf)Y4@BRpd&$x=i_kd}%%@quhB%Ul!4CK+0i*VqD2chN_N= z%hdJz%~0r+0&G&)lk|QgF*QW5E&3P?!M-b@bL8Iu#BSqmRD{pj3s=`KO(|1!g3lHC z(^Wue5qLi?cs@IJ17yEhZAjBB(`r6`ebzw*J@qI>pZ5XJ;5YxQ&1&xdQ-ud@BELhA0L}UfAFlTRoQ3Eak;3z zWs2nGxzHwai9msE#Eh(&S!l&6!C*o9P>vxgdt(S?2@Bz?aO`oK=CwjAoWPM^m30S4 zRctex+%PFpq%Ri?Lu2loP^VlUUoPzFG^-Xi3^C4 z$gp?`3%9q${T`ja9vluCjv%8lKz_A3d@Q(GBXv4 zDy4$d5Sb`7-lU(?CDiJ95g2gv4Q!Q!@#Y12`jHa2+&0ApwuglD^wcx}MTkW8k1R;^ z2Wr7druCorqA6mm%d|>MlHeJd@Jrq4xAK&2j+ML@zx6@OA{w){Vmt2Blro~hgqcv3 z^pg<`yblX_tdjj;^a>w_It4syPX=|5??;AQwEKNfy+S1N-S-BAy@ z{~I9eTjI$bodx=cl9?n6w0QKU-?Hqk5fg5#0SBrOp4`)oOW9@(w%v5+%bm^S>-V4K z=JxcnSW#=A20N;D#J6;H@NBu`W})=3mu}ba;5Kn!3Da zejx~K9k0ZFls(rd9Z@kwj@bARwB5gBrpGPLObScAX9`uqP9yx8Nq{GZ{(0Ls0)mRR z;5JYOqa+%3>N}_OG~yTu`yGvCh@^g2h42hptO!41_=F5+>$&eqJ(QFAfp5Akpz6k5| zag;vv%fH1k)(YkMB;OQQEkZnKzUN4}0%AXghLO^NM9yU+6XujFNz98aZLm?%nXUY> z4>81=QB`8ZXhGLl=6IWQFji6&X_jK34AKXYa$$012+o|mvP^$cW!9`35*yVkWza0c zgn+4*NY3BvgOOX3fbD5!qp`S!(3$*|&5C*Cj{pd$1z8zL>wsNS)1rbU{X(Em;2P9X zP{Dbnm~>Mc$it*a=z>+|B%q;z<`LUKx>Hk3EB2Fm5N+84&zmD5H)kIaj( zRMSDg;#-~+Rw1?Cy!Ud6%a}t1GXDO~AAdy;`8PPaAcmSoOp%mP5SqQ|K33Ki-)KhJ zzWt$EJbdZ6U!iq_n+UT8cVtGjR>3D*!JlAPmgG#ZX&S9+;pu@T-VGipdfV8)(zyqx49w#=0 zrpp4Q-@Z^N$8&FPUZtsBOdUVFgHGM+6>wmbiugnqg_9$ja0~Z3 zXG18m$CpxktF1VPX-fLVga*1FC53AQ>X+X(fq=(#6^;+pRF8UKm-bMrbgD*2-|1I& z<%KqFU_n^bMBl|2vAR3$OY%L)ivCkx%C^#YO@;+7h6g9~8^92G6vo1mY2qqMfg%fxfXfoMndNScq4^Cj7&&4*{bT8-!P&Y0ZPcZUkNng` zMoLWADP+d0DA=gkOIIsPXR`mnBgA;83i1afI3Hgp~DrT;3Vm7#z zThIRnpcSgK>~wi-vR4jh&HlAO8%(%f$>1AeD;VTin<;T=gmiaFPpkt5B@inJ$S&wF z&&%WKSN3xh`*;5a6lj6Xzl4sgqFZ~CXwA#I7L9ABoR0~to-8@5i0SHG`UCHPD%CJz zcCg3TpCmKuB`B)g;A! zEX0`bJh3k;gJ8*QG~bb}<9}U>yDt3)w&cYd;RU%tvA7c8{ncjX7ldVVBNsP1k?myB zPf}O4mo<#thCGR*kz^8jyDDg*RxAK*ep~$M%J-bXaNehmkZg=b!??j4Yy&Tf_8+_ znmJ)Nbbw!Zc%*)-Ps{8J68GM61?KFsD{E!%?C_+9gqf}6fHyv;s%ubRVPJQDAo7vS z60Mq6$*wKmW)HNr zM~OXXpwvO-8PY3_%~>Oiu&i_a#1+r0jfVexi6o|byvY6L%1LT=9b`0(W8_NGb6a7r zf&PUyl@1!=H=vn%by=R7pjEH5B=BdhcsHloa?%kLvjQ%hmhktD#qkUfbB3q+x>AAp zWA7B*b8M55K)QJK&kdvg%RJ2_(o7~Xly}>D!5x?D%d%dUvWSvGp!|-20=0BIaZsz% zSBkJ0mcX(DJKKHSfbb|UF&}DM9*53^XbpA^)4*RTBibgzo<*}K+3zW5j>8n;!9k21 zNy$*t_n8~V?;ySNjm?+(P1GZ8Z03{&uj8RcG`Mz%5cgRwCs2zk_O!9E}!5^ms&t-=T zer@~)7@dSk-y6Rfr{J?{B;Ivmmbu%!mC=VghI`Wc>o8y}o3K(|F*Fcu!swRr)!h_b z$2PWRX@i2u>4tQ?GWFe>1Ev4DvM*tVXy4FcmlPYEHNE^u<`Z}tL)&L87of7<;mXVD z!!L_XFk_ZyjWVgOsZwgbvRi4-ycYV&3DMR2FxgA|jez*HFWOts_ z@GG(oPh9h1*6`4`zDzD70NI{%Kgl`hj^L*PULx#N#exssmD#f0u^p&(!~r=7=lycy z@s$G0uKdq?6nM?`vmPC87}Lgy0H^Ufh4wngZuQs%3*1o#u2!VPB-^I4Hz1`C1o}OZ z58HWOoMjN&@*dZwlA0&#jefnSh09Z>vQ<(Idc4-*Lra;)t*L`a5)K`_CE=f1+U39E zlCG^+E@e4k*tG!)A}?cHkop4_2-g@+tl=Y7O4c*{wKC(=Ggse(L<{#lYMA5ZRw_Pb z;KNLG(_j-&frAQ14!QQpp-=I2qdXrrZWR1v(w{a=rQpNBhA-^uPV#EX6mVWB-Mm}M^MZfn&}wI)xJi9im#K~Q>w;k{n8&+? zm$pJim4l1b#`=rw>qCrL2uqGnFg7^p^qLOp78%tEZXE%bw>47jUIPKQ8JkaWx<8-J zfr9BPwgN=5bKBwZo&$-vqD=6@#ES+$p9&p`Ap2I4T(9=-Hnt{Fe*?x{{%XSQTcz@p zSoRBtnJk+7K~QgZKAQFz-y!>H-QqWZoySL8C|IX)wu}(^vhcmj(x9V)QH1n7!QLw( zv1lpfF}D?Gh2?~51eZXD%JPkU;E!87|c1#PU`*k?DUWyZhVW>79Mju z)Yb{;Z;K|1G=s$7t)xX|l%4kStqvS6vO~j_>+W^~%^GMa2K@Iw<|r4+5b>jgMK68d z=`!DxbCyR6k6I<@VCaO;xVZr1kRTe8jd-PKyM@`BdTNt0Yd=?yE<-)*(7xd-9~D{~ z%(4lFQ3W-x_kT%FFntrq@7|rjo{sp~hQM(?>5{QhMOztAsh(4PZ&qu&|2Kp;>w6~U z{2F80{S7c}0LQPq9A*=&6NA~A``b`#&1S8!K2&Ro^B}O&29svaUc$C)-7PO&uU_od zXkH0LGb_)v+G!3lp0wn;NnOsY(|+`tXzSvWl0m4c*=Wh8NuIx{q&a;^GCEO zF>|ylorZ;$j@!ubdlBF&(gb&qc@iCtQ1iX4hYz>W{?H_PXM+J7p$R9@os8hrKF7OfK(vy$l1Tq&4{AeV}k0YX%&Gyqov-Rzip_XzSBt7Sk9SduU=N zc);lAbRYESci~F?d12y8leA_jN((ZWNoF+CB3&(jVA4-x-a{l#y^iFci9wp?qlABR zKh1DQ6PBCMG`ha;J#qFYrO4O^M#KFPZ#Y!Qa%Vv6KuEt;Vn);O{@V|eeEdKr8=?L5 zS#thyI5f1*%f962xJ7V0?XazklIFeSbQ?a{ddOmp+`;QF2;E^xoQ?fFzxSP)l zVmBrF2$6=~2=*U)fkjdPj;Zbah-q2JceGwApIg+h5Rw?S@1p-pGojo>y_)b&13%R% z%|XYuEA8S)R?6$b-*M{8Ha$`4aQFV0Je)-0ngrjPL4eE9Te_sNn8HrZAwBy$bG717EcSb^F&CcPE86Zf@Tu`jneoSzB5GOQii40)GheDQ;q$k1 zvNH|TqgD0|<;cPjdW{QHEc&WgKs|OsLlInYW;B;y{*`|sb!fvUo;hxAUUmjwwVXvy z;`eq{O;1KQXx!XO#F$W?SLS;p$=)FY5yfWC(gQkux zlG&`?B4Rv@k9Jh>VERh<={d(q_y9yxQ~ zbe2{E&J1-ApR_R;(+uVF)-J9!x2@ArjFLrUyFaT@FrL;nZ>-_@DvRhip`UsyJ%&AB zs`iDh*bpXGmWm@*RBKO3K`BpOGmE%PWLW7<=+O=KL{G#0UXl384=MUWwvf_ucsDY-{<$*$iYKG4NMwUF zBGe|4J<=%Aw(LzKG+=YY>o-740y8o%JMQKMUJ(#T&C;9>k<8S9h@M~;1GG`Chka|u zub&DlI{GUqU6{sPE6A(w!y4(Oeph#(O{mdZ;svX6ymX-Y^x~Xkt->p2k|y%qI;cBi zg`U(RRSZQs5urc!(5P%y3sa2x+L}uSFYe%d>4PTBY%!9c%@P}2XRuAZdd-gR=dtU;GC;IE^W%@x`emav7!XtRKJt6%LI zxx+Q+NQTONnP7qN@FY0*T4ep6@M@Rj^a?0J?9}rmszm`qkW$XpBL z^M-n6Q@_Z#t~kSo);q{vtq<_>IQFqsec)!OiqD(YW!2>F9EbSE2K!s&KHk*;!F^q+ zT-8a5H^0Ik*gE*KO>!lEXThLf(yp7kDi{qfc)f&UNwk%H#t9A}dTOne{RTvCq^8j1 zQY#lA5r`&`9s4T@y?YmIDQVU@ivl~qQ;G<4um~a_+>nEKjFNF)ykG>9J8PI_Q}A$H zTB6N6@Md*|c{=h+KS2zKf10oQNrBf-jxe^8c4dk}VW(#Vg`skK4ahP|ds-=z@DFEY4n(ExwiW;MN&N1#P5rF*~`i9rgfqb7ei7s#z{>zv(VB z!(oKH8Yw?y->ALD20yiN);-feq z7akz=6=?+X;|jGDS1*tWTjWC)ffxc6u0#k|5`w%#TEk1l$+guaIoYZil1e1`HnC%d z%6=N0hs8H|W)V@++arJs^Jp7CL3}^lQxO*`X2xWbBBWNd#>#db^d+lGlCs&q+C^l#**kIe_eqsWWUycSI zj zygjZ4k5|k0q%J!VoH$pdO^on{&Y_BpJN^|;N$>KZg6`9Vp2cctTMAhi9u|s&CJL%Z zTn;*p*J($0va0M!WC9`T0Cm*MHyO|1qyuB(iJsh<$v#MSq+IaT=grG`S;~?2gO*mlXjxWQju97B>@Rr$6^`@<8fq>3>m{Ugu zX7#ykHzYR{Be=rgf*{d|ex+-Zmc?VPgLW00px+E{kR^tpZJ-H`4hgkyCEEep|Z>h2I#%? znzsuxQu&y&a2qKca2=4RQdizWBHUo28964H{hf+o$I$v$5omq>*?cjpeLXF+{q{ z@SjsbYrlZ0==h!ec}*)I^H&nEeVl{n=7o(PxSCho&KL$frEnb4j0LRJ{Tr zLoxDo&)?p%JNM5mp2K)#$wVEt5O2_3sni!2#{;>qI^J@$jd~~a1U0~#c3gT4g#yPY zDmCz>A)Qt!7#S3aGHqlO{#pcV&>||WKOP#&*&VX|uAzmwc}^bc=wTRbMfu^lCs+wL z(qm7zyV!Z9!^2qmvU#VSZQ6@kWN~#A&1E*JBJH0;GfI@vv~tS_nFBxK-X#WWI~cI3 zj5CzhMeDgo*jxPuL~DG0&g$Fdq<@1?}<@Z0UiS|P!W}lSUQGB zN~tUxzgQ-rrCOXfan>#)Q1{_gx*Hi^X~p4ZF#yXzix>CY-QeXafe2~Po3Hx_Jtp40 zW9hf(AF20PI18qdK^_7_LV+LoPQ3N(3OwUr2QkKX2k1o+gQ#NL1hHXwfnJEn*kihQD&AygL87*a*SrzJ-JGcn5&}W_pi&1 z-@kJ!SE(-9+otyxtv_LKSGcc$(!qDFoL;Hxh&FPyi9AZak(mSLA@bz)#bVd3XG`}C z7?s<~>B|jg7d{)EW=h{%V{J8jsg^+>A?M)<2wC*EXR>}WK;syuLeJ_$IH)HJ#tIf)!W^Jl&BkENPvm$ z@)rx-{67{3bE4mOb69#dnr&qYqnojTs|;D~=jDQv!8}n)3&<5@6Zr4U2_jsDWNVfa zuuk-*#b-vqA@)a~vAUQN73Bh+WpDd*;++8mkxEHqpVo- zbQ!)N366L}IY1faftJ-p!sa><7&de$A*KXF_K z89AcllGD-;WOO?sJ2DQ7xee4EIcths`d-na4DXmpaxf_N^67NL>TE&6TJks$9M>S^e3toN0$H zssPktFw39{tcJI7ubWJQiR}Ln?S{e}qg$c&3w3|x)*~zKI?gKiMQw@4;CO5UR>1D~ zZP+Ap+{*fnNfW9%_u*8CmylQ<^A4FTP9<|R)dHpmWM%||>l~4N) zHhaNA_9{R4U$nO@oGmw0bGf=Ki=?_P`@obZ(v~kDHjZFw>0@cAB$0<6GWOQQC!&~N zGrpJl4WMsZs9hm5#pWoVZNbsNc~assq3BvGFl~7wqgzNZ;Fp(udRr0Jggh^Fy<(#N z-h}lz`Y#OB*VEY~t#mzn@qIeZrZmpG|8o?5p#$oQi|LonitMcBd0Y_%mX33zy zNVu%bwvm$qR=DgK6r0-}*)vnS0m@&j9kEp*kQIJRV zd7?9cD6Xt3RM6940;-*L)_9G~A3hCPUzZ&?T(+@B$0w=1w~oZ!!|CbssR4f}V}02^ zE2Dr(ca@P(R{jhJm8^NESVybH8G`j?$KwB?>n+3DYT7Q);O+!>cXuf6l;Z9QP$0Nd zDDDs(in}*Rpm=fD0>z=Y6{obNROkuM`@G+|zVlt@&;GI5B(rDkxo6#Lt(k1PO0o9> zMS&K?w^*tCk7B=o*>i#@=UG)9A%Szh90U_w_RbC0y5Ixx*k6svQ1Ln}j5Trwr682c zrm!WCX~sn}Lfe5B+uaG(9vnpuYnO=)bRYa4SY&P>r&!{~-r_nPa3T4EDEdH0k7i#F zK`DL4!5GIJq96tJH3i!11IgJ7cnJZRUUSikHrcVjvxJnCy4sMvW7+i_M+SjQ@4iPD ze|lBzH5=w&{zWL(ziIrU3sHLhl>YAawW_olRNXoH;F46rtLNj_q}f|pT8&t`kN*M+ ze+zQl=_ETemz{icPPPkkh(FCJc3S^(M8=qC-UKdYIH3D%Rs1CL|I@fC#B%bd;HP&~ zy(E-s!bR2(XEuMruMJO&n@=?td;boZ>p-}E?Yv=b05JQC@AcfLMq`tTIKyQ}b(_^- zYLkQSmJM^vpje;r|8ES;E=Y>2YYY`#7_V1ut19GrGR)ZVF;T3zI}QzwJ=C(ZoUVMo zTFCaRNP4N=Z?2KjmO*G29k1PeVLTTP`JcY~r+GQc;T_8LRC9G_u|i}D9X5P2_`L7G z<=v?I@!H2vlM%QsRduT}W3763rP>Z`d4$U-<%p9RFUDGa^!r$^lOX9*S<>KVnx$gd zN@UlvokBNaI_F>+a`L}Ic5;X%x$If08L0H50QYwsm(Eu%Ydhu}YJ?uqEh~ag zg!N=2{)J%fCs=5AP!yP&XNn03MKMz6ntrABn*LYT)5e360nR5^Z20&ue1MQl{M}O9 z=V_rS!*6~McP16su&rleFY6#5a~!fx&vbKGI#kcOcfL&Pt~lA1$T$+t2;=@n&Wt1+ zkifK}YEbyBsLgF+nJ%OfR0x)s6B=t(T)GqDZP4T7d2Yq$e?HMNmA29jFlQqykR)W} z)bi>o0)SzxHx5$J3SHHslvY{YN*tLY761ChOX4o44w3opxRevoKt$f?JQ8i6k!3*{ zNUcIQe*yDVC^GB#4t;M)bO`o74*&3Dk&ET>OJ^~GPt?1=eZ8{=t}vn2wJJDllL_|{ zYsFE@_}x<79qFJUPKI`Pqv5d4A9)x1 zbo?<@Tv@QH6lOvB97@RBEP<6}mVoVW9LD8oG5@9Sz5p0Y8Up@Crc z+cZ31>N%%%;=uJ(00oKO)Bs>1tmMI@ehI>&9(zW{AaQjl&d(nkZ>Vr(jY#FrvlxR@0tjy*5sb|ZQ@$J#hk z=e0rD>LNYGf$k@Y&!R8$gGzeypa4W`<`yXAj3BVT0L~^b`$tqrx5Nf?#88QZ?l}4U zM_nD<6g=-bCHuK{CGfr_JcUdCcA~@JKVPq=;QPA&i^*D;-1!4RvRAEpvTuIttjve) znX&K?F!ZHUGJSnFYdW$j=+)^%unL9BLp z>CzYwrB1}II3@_!Tz(*VZjXlIfd)<)x`$fX7 z!jbC?FpGd!xJ4OL|B!6Oqj3fHLioDo55yMWl;d#hb96jG*V_~vHB^%Q|wDtoo2iKUf)iyV2pw1vy{ znDXdcEw#w&v}qCKZ|4d%ciOsRKF)*#y4c{Je{R3#qN?zWTp>3HmVBx`R88VtzVxOJ zY|nC3|MJPid1erTk+35FvtNrqI=8UQp)CQ3yU@c8c82~a$7I?~EsGi< zp-GNTZfDhgiQ97e`|??lJ~wM0SC(mxHD zNLq!F44Bh9xKXOfM_Mjz2T67znJa>PVOoVVsz$uSzn>%ABt3#_+<-5QCjpbccp#We z$C1==vmrYbQt$c*1 zFpQQ;eZjVLZt8>!mNA3gnZ+iPQQy}BtVO}MDEcgCY8V0U@!N30rDaVMVn- zUl8UtW`)0f>#y_L(_j4iyuPdBrzP!~gU_zl#_OK$=6TLx+sY#E9SgPtUFAq|9wF;= z+Iaz_CJ%M<=4LL9;){;XshD~d^?9p2XgDwiAL;SCMR;(QzMcAB+FyWqwEW1LX7%UW zJ!L|bLDKlElf|IH;O1X3;~R zT~8yNLaBZuQbQ0gO$1%;U0J}@`QY;gF`T@^eHD=ucEci7E5CBF2kI2y$80CN@8TP+ zvZ7J(tPUTe&K4hDB-2qu%U7&?GJVb=qYg&cOJqT!OlO7`3#vtnh|MLDoR`$I)GX>& zeWY3%%w~q})L917Q$xq15UvH^Ji~0o_3YEM+jjVfhk@$t9nH<9WmjF226GO0Bp+In zokj+_C_f3;=Ulhf3w4-X?txM=V1=Y#yu9z^)kz@E3Ua0N{$%oL;Xlc?fy7?7{5PPz zC|%VQpWX9EK5J$Aq>PqeLmkD3jS~y~4H@+lwO)8#d&(<0^o8#>Bj%r*Qs>2xQ_DZt z-;?VvuHW*`(FcGQ|0EZ4YdhVdxbS1A=~;p5y3K+$^-^#ldC=T{t5?C*Xa)8#@ru841(V2^c0My-mJ& zFnM8vq>-;d!Ie0!>A-9w2GF~!lbop3U+8TJcEfn5C$lx5)A5i|JpzkCuXMmUO<9%_ zq-b7LOp*6(TeGQ9K}f}_Rqbh!LqA{5^nndpCRqHNRi&VylAHYU%tAYNKZJECLep$L z8~|a-|5h>77Zw!Z_^0M6IdWr2?Ul1KO5$tbHH2=YuZXi3Kt+kszc?S6rQ@JF&NR3! ztpsjK6)&i)a4t|}F$=8Fn5MaCm}*dh=Loe2FOVy7xf2ktcl6d}drNZ1RTbj4 zF>QJNB`TEgULMteFj6)sj#xwQ#SjOw73ms0c!f?qRhtDA_Uh~V3vY-SOtF?$7ATC! zuV(!R;qza(FbD!C&=1~I1ySO1-KRymYwV(F3Ej!8Si44?Z{K&ya5?8{l6GZhZL!K^A#caF$|D97I1Kr#f9NXMkY`@B@Nd_=y+_E!%tEj&qRzq4U zQrB4l{sm+>%|F!g1r|yjlam8k2L?vH0QrEzZ# zpST-luqU%scYDrl?~VMa-cO($lYvn626E9K-}cdlzH}ucY^LT{`{Ps^lW^@zIYScI zDhGqIZas2)sVs0x@Y8|hGqR-v^Q&?tWK3o+V7vOD`xsSj$hkw62R7i&)RB8zoeIE^ zL0r$dgroah4O6u~_l>6)dGa(vr%oxaOJgH>erx6^6u%eta{ayV4ni z`6o(sJLore8GjZ6C5!kuy{|a+%L^H64mng#FOcGcUlUmt{RK?Z|EZTKqp_9`c~c|2 zuR6{&_-w&?^!L;A0)YdP-x1!&cdgxeF!i81hZ8DMZsI4taLV%5h)^^sFo$={CdEp`efGJYNTlR#q z##;hfdUKA+2lMtkqEPu$YGa|$-Nx460f+s7tne2>d5g@vo6&QV zKeD(>R1^?dncHyw)b^dtknUC< z|1Ta9{fL|Y=Y9o(AyQvGQl5d-s5$aT`OgI=k}ixBex@=i)+Y1y^|*oVGEEBf@z!ha zIhmyH&`f|M9OGC-0Ea3AL;VV+4?0b(aVdJC;umMPbs_*mr#(*2%F06BGGVpr$2G>m zAnrNTUq~V`wG?0>Y*9vU4kWUY3_J&7TWBK!crS-mA(LoI&un+~(E7Vr`2Q^}}frip3)z3UG=eJaN5ApKuV?M9e;E01A#G5)EHP1Xw zZ036r1GNx4xK7H^ExUJZOpS3*|G4~YL3>4%QkP0m050Hc302+E)&E3`-Mmj>I?pDg z5vx7dzvNMi&L%>pZ^0d5DjE_0cv~PzJ2Vs#!~$uh9S#<(!^+`U){r`MKryqqGn~uh z6qgB%?e%`W6Ri3jV;6!mGjjNnwN)74p&pbhoj}`RDS+bWYF&~Y!) z;)7J9aU#c^Eh(;z{~ES;G^qwRZi16(c{ar!>HXN46~-A)8n{6O6zaB1K& zIL?uP%x@V zIl{IqT*VX=@B(hC9gEvpGjQ^yoNM=c&Q@s`Y%z|ldQR+pb^xlho&?=TD~b0*-G_8u zPqBl@R2XXTl-`yfPNxVYy%LsXK!P_pe6&nwb3T>^VzAPLxlT!-H)?zT1sr>#;V#Y` z=%q;#&z zAGXb)gf^?$on8MEh?Nm6uF=p^fi|!gqRf4wZG?e$<{H)QpKVMg*2D1z}}Pvzi~L@P@S@C~Mtp>8L94lWsvh z=S0Hr> zX^|R_aRdBK=T-iZkH=Thhk9X=Vf&18_yra10!i%K953` zcoJf2xG0fTCwcjn-#_{J#3;};r@cO3jaUL?z1i~)^is{UG^VYW$R$UOfX&-_i?_Q> zgW0WqXdh0Z@Mv>QYQjw{f$18uOq5~w7m%avD&0X)_Wyox|FevT%eSmQ0#2b;6lU2m zgTRDLg0OluiSjO^^*|m4otaEsvx!#qu`6F~E+5IujE%d^R6h`#zf-dPvj&jDmb9D! zUTOpNS4&q>9A=vZkNQvb5b8Yn=DGTlV+iRNfFIu|?GzVNIwKzGa~{hp7SF$cMX!po z4Hyc_jp$@rsxR;chDy}DHi+&~@)r>PT!A7;ommf+vr%{Ck=uj=6b5{oB`BA;JVh+A z$9aPEYblaPmwbYt{2=i!z~#uy^GXQ?gok)Uw(~DwYG*?}gOa8WSG_I9j#-6cHcx}y zWU**bjm?B(R9&wY`v_NOzfIN|^m{4#60xleB0RQ}#n3Hi0HGS2JGE+TU?zM`^7V!= zEoC}n+^Z6Mu^LoiyO1)%)yO!Y#|%I_dzs$P;S~THTcNN2XTg}vRrMcIoSDTY)==<< zWO#-}X48w@at0(58)dmMN07SSN;v#5Oia%z(OC4ymUZkhzzh2ZsK%|qJ|uLSxeBUb9nQ>ao>5oB>#kB?&GX7znr1WHc#q#fEhF|DO^hT(_!tdo@5(`HQ(dkGn?0p`CVl&M3|`g-*G;qNmk;Y4-~r zzVI~3DAl#2mkT+|n(Bi6u#_PK20i7BgdbPcQkH;x0-T6qB;2f6yoq5Aq=3;5UF8ZEj zUOe{|)l1VO)f9Dm3_ZJdD9umnvEStH<8JSVlvGy)Z)5)9EtN-rbUn!U-gK<;#hzt> zq(Sph+v#*%vG7JRCqMK#O=*4VwR7?l;N*|oG?q63 z3E3*E2|w|kP{HeK>vt_&0XDDM-V0$whgFJ&)c*8RZ|ptuQ2ors-QeO@(8FZaW4d-khDx1M^7G6&30t|9L!S8aTT~e`%ey8O6Fu94Nx_ zP5kr~_3wH|=+gOaFoiClv$Y9>D!#F$7`@x=NTjcr^4qV}+JzQrADyed2J|@tme9L1 z;JFr4>5u#Y5Nj+Ct``c8>#l?2ve!S*MxjxAR7#+`WoU-L3%NY-(Axj>%qZp%45Rvv z00P;tpYZB?pYCc_55Hs=IuVg`of(m#ICvM9 z0Lp7JOg>AK$+eer;@ZnN4O_-}bzncCa2z-T(nx*;>&O~;cFaW}KazTXeoWy#LvLNH zOo~}5@z`-1mX*@X=5)#DGR09TpTr7K!CHix{)XX=i)<{Ns7+Gx$Rau+AsnSuaz$rY z5lNQD5mzj08xJvo)htWqAT|8Cr%&&iwH-?(!29@;GNBT;S->=6y~OR2iU^&TQXb^y z%Wf(vt(7okVqB>UL6ld)l(I>$1lV5QZ@*cH5UT0>iIyho^k-~^vsXs=?4I5fLDLMI z%OA625lRiXRlVagois7pLP=WFTVlsJZsVC+h$O7Q!TC-IA!hx`ONmfEZyWWai!p7cOUmow=*~^ej|)ukTGxgB@=WD$l%_`a)4E zC~38UC;4RG#a@}uG%`_x4X*J+>jmsAPXU|UWGH{(!^4}u0RIo0h%b(9LqI~O?}DrC zHrPh4hDlzpE0SzkX=ypL(%jwmrMf6vE`#{5(G`sKbKmhYZOhHedw z#nq5xKYLH$Rj0ULE+3--_w0_s&SQr9w^Czw#*nH3e=@)3O+Mra-Piv~Qfe%Bq6x7( zY1{M`SaUJ-W!lS%earV3fFT+4JbfxdFj=tO!DXas;49b| zZS4j*Cf|&5VlQ_a5!QXt6GR^{bj~;%;)++@qwr#@)D-zB)frZ}@79Mk5Lg`Tlb0S( z8JOoKT>`)hcv&!1Wx8D9hdVZOXn32k-H;1e+%9Ilcs;V>vPap(NS*GNAEL8EzgR+Y zKmN6*P%FKK%v05Eb=kz&U`v7YHJd#Bj+M5J!*5Xpbx`pi>~1^*x4)X;XgXrE)ghsc zsW(g!rCNu^pv=OJw63vvUoi`eSf&h>cTWXUeeTFQwU{J0$?O$-oC<+~efz<628yRP zBE|dXQvL~t^lY%_K?_3NWJnR{MW$Zp-!2i=Fz0KmH0EObk|hHRf@h7NFsRQ|{^>Ao zl81}EstmDj$miIYwK=-*9N|>uaEU}Lp2Y3KICC6#8W;Iii;pp>l?1;N+btk{)5g}e zlcz`DJnVw~t=nb32;IBT*STm&1j(bAvD7u{j69O)XFCijVls^oMxCn8o#eOFvJ|Md zg^vIPiHM&+2O!1f5bdX#3a%YQ8RHvvthTN04jW;yx(!%CnH2!y9DeEu*pIJfcZhh&x7)zEZ9v}wNdCb#&eekGO9qtO6i{(6GC4WoxNX(rJ&4%LT^fKOy)GdVLDyG(Kkje8dWrZ zXY2rt7lXQBF|!kS(rRW1K`AsUtsT(q6^BYQgdFuz>a3RS5~1ElQqf66%^!!(-`Cm1 zi^h%21g5Felho>xXnM?z(zO)wjx`5HBm46E+S$GShBL8~#n{3LhENT@h^K9wr3O4B zsftiPwr$^%2;uaDA6HyaZN8wbj&$Tx#Ay*+@iV5<-NL3`a)-`!wIbu$);I_gQ-VQw z%C{XdC){-g8u{X6GHS?NVk3iH2jq6wyyyn+X^o;iYg|*X5lf`xQ81bsLmC9rwLiOfkzZ}j+q4Sw4qz<@b-nJ*_x+OB@T@XPz@mW6?lbB9z$UY`}s$c)ykAGAQEv-JCAMpU^~%`6uVLKKm#ao zV^WkO6p}ybfaAK})T%rcf8)qd>K4FMZDI&DbB*xJG-wi>(;%}<3Tb;sojA(A#}{(~ zH~z+NTYL*MjrRqspnTt2;nD1+BvO~-rm}uXjP}stPTlq*VHsqkh@>KI{LT4RRi4qZ zj1>YLz0E4wuMOKz*CcTdLQDKLluIw)lK#oyttyRZB-c6Z(x5|f zMrF8vk})H$w>mH!sv==#P%x#A0f~WWk_H_Z(`;Ng&TFZ!9`DenB>=;E$X} zUEVc>62l_|Z<*--gordKaXl7r+Dgf_gX&Eo)Rs-e5ZhcU2`910R(m2*#=ORC@#n%) zV1uQ*ue4t5fu8hRO=cwC;ItQyX9!_EKnKA~^Jawb2=fQ?SN(Es{PB|W@neH8ttRe= zZ9iUuzkqBSj!?9G1lE)i)UX{1^G6(CQcgWG?J*;xdjS{9}D?cBbmO7)qrjqKodm_<)x;3y=1TQ4^^qjw>WX3o+1 zWt7CUjzR9Gn4}@H7-F`ygcDzw)Fa!55t$g_c^Bc!S*Ni(l%ysi6k?RM9O3(QVC%rZ zM3JAPh7(nu($fC|<=v8Z#in82q-aBVTAhqrUtu*Jw@n ziBD%vTO+f;U!+@xG{;n;T}a`qbHAt}oetu|nQV2t9<##@mu)B1?X(N>(`wxrTt&1< z35c!?>`Y&{D@pO*^|&xxfm~#6r3D><$`B_cvkzzzFk)STWm_nPX9=MnR#RYSk%hE48p2y#x@Qh@;)4f5dx++0U! z9oTCir7xoYU7l1Ysf6Jddau46r5FB&V7B|=@-sz&O$~n=!MH-X7LPK-Iff(5A(Irx zmDid(F*?F}Uq3O~N)x7q_zK9(U_-Bo&3hAntQuzybwdtHEeD0vgWKwEE9^CRP9mG9 zw5g+%0cexbfF`{o$!80-y6|F?H-l<1zD!>dzfbS|Q}e9SPjpK`5OlcBfSx2M)ENVi z6{|lt$lRNSw(MhS3c4rcj}c%lu?xL4`oE)9<{wDIvA@#UprX$FUaxy9q9^6{W`3ZN zPLT5c+DFMKhgI&~s-g{%mGg#CNS&s^g1!LHIDwVE+xDlZ)-}osifXn>nTpg z>v3F52jSrB0d;34*6%Ha8KPWk$7j`hY@oR%($!?(&C7pHz^g+wOG zZZ%T^9014EH-mN_7@=aUq&$Oxp+-NFuj*( zKKb*9!+R35pE#!=0~|6gsiip6-&XR@`E=smE1YYABm3YzZpa z4yf5QNTlFcGBSr`LW}qL#+E_?Cl0$3_#3*li9kbl6kXsv!U(a+ANv&WtCnF*r=Od$ zOw7WN)Spvi=8En9T3S@&VNJ4sJ#xd8BO}!ex9n2*@r5FB!Vw4@k@}m;&nfp) zxtCuri-4tRH?qa9w(8u4`;0%w_6)>S;j<{rEM?_?&vDK3ghP)q#5{je9m_u3Rjyvi zHXzTyBOiZ3T>iz22=IqcLZ96<3*rp!1na-zQV*xS>^()2vMO`O8}3|iI*EO;537bz zFK0{fi6uUfW#q|Nnk;X-22fMeD~Gx|8Yet{VFCpM+G2Mr!hv-9hRISz?QTu#=9bbY z*kUc|j>!3ojN5#e(h4wq8aa~OL?KPEu2uL~u)Zq>fP>Qk)vTS+S>CysNfCX(ioas5 zCbqZ+BhApE634m13Wh!g6x8<-~7YKJu0UxpZNO`%(bvJMJ~ z&rGx47luTQmke82roaadAQG6$-V$br_QQ}T`3%^2)Q10uLSfD$9ZU7u_o~A0lpJP6 z$h8-Up8?~pz(F`HMOuf~ss94DM0xJ~)qnQ;@+#LR^3<0PI7Q?e1Bhv32VLUJd(a_p zH*miyZN5o6=;>B+x$qkXxSKsve-GlO877ZXb0s&W_X>}N*>!D>`r4D?P+813^4jJ- zp560Cyus|&(An%*4d835(cpME5GO(Z!Tm78)9`f2$K5|Sx9X9!Rk$Kf|HJzLeX@kh zTx7L+chSUQ3182tps|f^w82^`bm`~3Ss~C-SuTaaLIMt8oO)&?v={MfHhD9)F|EMO zPr5DAjEY)0OX|{Jl`T~@ECelVx=S6>I4#~dF1ZMuHqrn_{lZnReh&5Tw--g5KG{cr zwg2{oR=yZaem% z<7Q`bYyMB+O&z`v%ZBKd#_8YyL-)d6qzLbU&Az_r9}wNj^mf537~eCM>X8vCj{TkY zc=8{mE}T;!A)vZ^`xZ?=%*8pFdNW)M)&?v`dv3+@>EbLF+aJ8iM?X9??tQdr+IgZ?Iv-X30`Pb=W`%S3 zUW~O*qv-prWwg(!L`E&p|5BojgT|hu8~%oR=TNR)=y7HKuPaQdvkhV z4{Oo)#e}$kZmwfyKkf}Bpi*m4Za^QO?N5@i8Cp~yfzSvYGG!q+1S(U10k*5pg&h6@ zf+13PpE5pNH&*`zpdmF7n}kA> z<#y(3w@=Y((=s$ZTZEH~5|2|Qp&{nL+kfT&Q~7%XSp)hOnb#YxcM}-W?7#76DRTF% zMxjkx;w!u#oD$BdX`qn9m4+`6T8aPt2eJ&Vq_*~^K&eshjMppGGZ_rN?p#B=D}vxO zfOp!s-W4#>#I&KPt=8gxMO=~=)sbFmjJvlfEwbT@%fD>hn6N+0&1JMQ8Yg>gc-Pf; zADj?CCf4t{W7_eooE_%sE!vc6-Z2UDea@UrC`|aG^^@t>r>dPBBAToc8r~y53879a zx=JoD{EuTArav;rR~qRqOS+!%+^`WkYLw~FG_!FOLGxyO!Zz1yn|Kw`#Rg0bT7VBS#G&2zHZ z4d!5DR#v_CjreN}Q<%Oo{>-@96^a?m;{Hy@WrU?}RQqzLf&xC7C8ket_G}2Q#gI5U zf$Grz8XVR9-NiCwtbV-Ir&)sY_$TcP0*1;V$|;l}#<}(23ul2{;cw^F#R)nPW47fPwrYQl1fjQ2WQ58Z0WDR!`s*hgm$p!L4Sm?8{i9SwT1uVc4>G z@eBuAh7`sNjuc#D(T_UzGkG$<`-$H*#u=)RJA!R7oJCXS)D$S==HRCk1%Ta}g3sOc zwOx*40Ap!Mx>+;C{WI9>@)m-;C%ZT_+UW=tKiMeOcu8#tu9qy>TLsQ?5!*2xdsZ&q z2FkE-LovX=arHeQqAqJFIP4HjcDNL5tGCY(JBCfT%e{Z9apF!#d2ZBMn>P%R@gy~v zZKsC4fRVPF+a#W$=v|5;;BKw)<7!meXeXqq{|HV78Zuv~IjBB(lBO|s)Gk6Q105A9 z2t!rWt1(E>TIuqOpo&L+OkWExfSdsy87TmFVDv<#rB5i`ZTF79dYsp3DS)=F3P5J{jcjIx`ag-{`G@O@NIuPJ(y`s? z8Oq|u1|SZ=OuN_Ms$MCTu_WjP?;kXg%nyt$5wLqC4P@$9P_}NhSbN3@j6=gjTQ30% z!OwMT%a;{3(@fNFJw>B)bT!zNY6Y*|+TsFBJP~_CK4#($nf-rBExPH|!c zXa7B-kAS!KUY@Q~uB!EeFB7h_yGF?2@Fi`P*(p|^oV_;sfY0Jp(#K(5oE|i4MVVck z?mrwqJPy~rwfDw-^5Q+{p%P*;2TDmDKKdwXC@VzkarCwlkE|J^E2jp#*h*gp8|GPs zK9?XGitbF<@g#3X@JQS!OlrMt0<=fYDHSiZ=XwS!eA9tX4tj2cL!^#^RmCFJvWt0(Hn@kYV##Cl8>8wK&GkuE>2ns zW!z9DD2@Dhxvv^x89IfqEtz}PQRO&FhgWmP8Oto$eA&{Lhgaa#Ww&gX{$3X&W}Inb zbh4MU1GiCo-_w`70=h(wJmXiOQpP`LO(Dwu(#^V`mgBf!u8o)uazro8v6^PLo;qGl z4dEbc}y?={mYEG#c`_VOrsw^DQ`b#x5Z+f z*|efwccEiM@2ldwns=FYnv|Zmcj0tJ7b3iqin=!lJtPHzs!ZT1S>(}7<7?LUQfS$U zy**Y6BBc{CV4W{pmPG)x6O@R@^0=opt>ScVU(e%CuF|Prc18Unuch&4hR1OEabQ&^ zPtI4&6^^qk5w~05Wkr*|MwWO_Um6z&k|7QN4XKz?x9-A2Iq}?!pa7|&QRhVTd;f?> z^=gXntlCu-f0gu+ju)qUN|mV>s_DvFC3h&ZSM~c`pP3HNZ~rL4Sw5X{pxM5OQTZxd z#8b4p3!Q2X{a$ShCtkQSmwi)+``zj>6#i6O>P@y4M^=bRo5N#rD1LD3x?l0rffA2e zpgnoTOXzUjuHlyPB#~u+K;J)aj&MXGyi9n$`aKy4jv5jO!Jrk4lz`N3kE0EHl>L+Z; z3qgJAAXPD@?ip*lpF%3i3jT<+Ygl%`l60{mqCgWL`nJ~Ymy<)H)7{}|nyZ`1)S5B- z?oezs5z!A@V@WVKr(z7B9{`d~as%})4<%~URU?vhEf?aqpvE`ZZ&0&LL#MxxBB-De zEEC4znLv-OQIoJ zL?b!{9LY(m?J2#@3m=naxEmEtsGhQm4dl1#Pt^~ZETO;7s3cW=RmhA?7_g5lb@r*U zE7KbSz}{}!_9N8@pC0_bLE}cZ{u@%d4gODs@{}*$$-LZESa7XFf%i?LEwp(qpxo$H z)D0d2morD68ft}hhmBY;VpBnoP4<+O2KLwpZ_iAmTcz@Kb5Xl3RcJ@{`?ja?6vbkp z7vLstE_N#Bw zi2=`~(rGbOy;<~ZTV=R|K8<2i+at5nxlrJfn%dZMa_nK;tAuu?_A82WC+Lot$8-GOxS9+T@IIi&6=`mtO!jgfZV$;wf~*RCU1FM4qAS_ z{WqQbpU4oY+Vh*_!mIS6E$=eGExiiB++l0enDfc6%5udS&}G$%V%$w?NO`I&P5#XT z!J>EAs4ySqGzrdL??MLu41$T3c2{X2N)Ym{9vqc0SOs(tY4OULaT>g?Gl#`HH{ERN zPK7o|#uU*aIT2X4QeAaC^Qt`SXH_88k&HA_w>Yuj58lt3BNZ&_0UQS#kh=aNXAdqS z9&i$`OJMPL>3c#5IgO5orlct|=Z{O6vd;$(MTt$ZzC|vbspM2Un#kkJfkkEw3LTZE zH;cAyk;9?iIPgS>Cgj&}YNfO*5U#lWq@u zL{lDBoEi5tM$OM=(MHr^A)`-T_P*60&nCJ1ml2JyNcx}r4yCs>tRRQG2Z3?lo+l)yJGhg}~!-=AcBn>0pOJ z7>0AK&MaxdGNT$NEr&(<(>r2U_G&fd&3ac^IOq zNkF$XgaG3N$(^RQqJmL|Rk11)vH`Z!T@ZZG%WTa^g-#Szv|{$-FU=0*El9c)h5LC! z8TjS(;`-bbb+|A-Op&0|1onIoN|To~Bm*9XX_}e~#5laY4)UXqT>RcsV)lVVH;+ZA zYzmVq#+H^@L&1@!J2pN!qm>aGlbWjQ*)sE}^40nq{kO<3l3`aEl0*mu`YO}%johF+ z7ka`b=^W_|S9x=?!0%sjl3ox&1d+?G!#4S_js<8rvl@0|GduGBf=?a(lY%V}H>QFy z3rDQSn6l#dkc~-Q++;=d2M>~M9lrx)$DWr_E2juXR} zD<~`)IAQ$@h!}7>T>Ukzm3=TYlJcI?5Sbf&QEi6l7)`ZV+ipf>L#Tscd*J3@M@v0X zeBxtL8Q{0!D(i<^$`FW?E*^ugw^?a#V&+1l=IG8JvNlfQ_=4OB^%)y4ux0*VK#;Gv zc)Y?xm)oy~byF-B>U=_bj3dheDgTLT0oiY=Uc&ISxhVt-lZCTFRIU_bkE<><%{f0! zbJ=hpmHvTl7-6AePh?CBS^PHnk=?bT0Oi^s!Asdfd|5t#SJ~R+_)G21OY~B~F8MtT z3_Y<=RtQi&ACbQF7GYsqQsF-$B=O`!Y4A2?-;c)5%(JYXh!);%e*eWiaE<9)?j7{iK5F!nNE!0~VD|I!sF>29Hq#j!F~fQhw3@gAZ2B z);Ij{7ci#GB6!$B0y(yAqib(=1RN%yy@HpQ-*a@FX|yh50Q)Lfcj!4jvc zU183(#uBTZw{rUeX)u_EGY|mMA$cUok}OcJdz1g6b)TE^gU{B^(xmH~OPR1SjAk5245gc z9qlzrb!p#IFD8k`kRUikj!Sor>C0rFd!g^lKmR|IHh9l8nfE*Y*VXCMM9a1-?M(r-kwAk4}os8q#eQUBDUX$taRIpqbi!kdLA z$-EalC7T=Za;abDx$q+Q*V94Eyo$06+C=)zEnGiw7p(K?k-N#cJfuUqW3gEDN0atX z6*o-I@474Z**CAGc+&3$H(g^KrJhgelb-%K0$4oU5DDQH*~oKAi(7ly=RBp(Q&N}s zG|IYi>nCsRcyMdQM7h#)ZWp#+cj8OBCUdo8#Uz;k@$kTelf@2Ssy)H?bPbzi z>V~`mmxb>lV(v{b+NNaSD9$1#dNRVmafgz~`lNqShBsP6#kf=w7!WIxS5>MgM2II{ z`*}UM-s<9(B`p0L=ZF3iUFVs?rGK>7p;vXUWu#-7DU-@IOSQl>_HDOaV^zDHmUpC@ zzTR}pzqV8=d(qB5yCc3%i-gkEXZG%mX3jYhuKw^;)U{_U6<+LLF#Qp+{z=hGKwjQ`!;GUu8q>n-a$ zf=9C#>`Mf7)GmT1G}Mor`=D$rxMq^}WW{wpZ5v(+ZSjfF`&y0Vd?G@4@JsmO$^dVX4ovvuu9j@d2yBES4r_DTbtYBt~>ZDw(h(d zG_y18?EPr(3DsS*uRGQp>TNgz>eO`q0PairkqO+84w^9XE^Bg~{=igfU&Is1y_Z+6 zJgmGTH6f3~w&Y3a>jRaO0|AY+W;Or;Mm+?-{m3 zFV@thEvsB&HT@TZrmi82tKhAM_qJONBa@R}Ex9Sb;YyLCk#zT6kGE{Vp)G0d1!^00 z72-n~7~Kzqq{?dw9J}q1yC(bEB(c!%+k=cedeee*97RtRM{jAWOY7U*mao^hR=#aM za1RxzCBI-b6Nfd2NzV^6^DwDXWu{3d`WGB9`lBDQEk!c33ZsaT-n`89>Px2WdOec)D=Ki3u< z@o?O_X>&+AFW;wsJKsh5Y&@o8zw)j9Ki~fh!1G^31R2rpHHTa=&cw)Q&)_*9j|yg} GikkpzV_?Vt literal 0 HcmV?d00001 From 8d07844ae3ecae53dd7e0066c5aab34ef2bea093 Mon Sep 17 00:00:00 2001 From: Waleed Date: Sat, 7 Nov 2026 11:24:49 -0800 Subject: [PATCH 04/29] feat(library): 8 Best AI Business Process Automation Platforms for Enterprise Operations in 2026 (#9769) * feat(library): 8 Best AI Business Process Automation Platforms for Enterprise Operations in 2026 * Pi Babysit: address PR #9769 feedback --------- Co-authored-by: Sim Pi Agent --- .../index.mdx | 216 ++++++++++++++++++ .../cover.jpg | Bin 0 -> 30350 bytes 2 files changed, 216 insertions(+) create mode 100644 apps/sim/content/library/best-ai-business-process-automation-platforms-enterprise-operations-2026/index.mdx create mode 100644 apps/sim/public/library/best-ai-business-process-automation-platforms-enterprise-operations-2026/cover.jpg diff --git a/apps/sim/content/library/best-ai-business-process-automation-platforms-enterprise-operations-2026/index.mdx b/apps/sim/content/library/best-ai-business-process-automation-platforms-enterprise-operations-2026/index.mdx new file mode 100644 index 00000000000..9736ee74ae8 --- /dev/null +++ b/apps/sim/content/library/best-ai-business-process-automation-platforms-enterprise-operations-2026/index.mdx @@ -0,0 +1,216 @@ +--- +slug: best-ai-business-process-automation-platforms-enterprise-operations-2026 +title: '8 AI Business Process Automation Platforms for Enterprise Operations in 2026' +description: 'Compare eight AI business process automation platforms for enterprise operations, including agentic workflows, RPA, approvals, governance, and deployment.' +seoTitle: 'AI Business Process Automation Platforms 2026' +date: 2026-11-07 +updated: 2026-11-07 +authors: + - andrew +readingTime: 10 +tags: [AI Agents, Business Process Automation, Enterprise AI, Workflow Automation, Sim] +ogImage: /library/best-ai-business-process-automation-platforms-enterprise-operations-2026/cover.jpg +draft: false +faq: + - q: "What are the top platforms for business process automation with AI?" + a: "Sim, UiPath, Automation Anywhere, Microsoft Power Automate, ServiceNow, Appian, Pega, and Camunda are notable platforms for business process automation with AI, with the right choice depending on whether the process is agent-led, RPA-heavy, Microsoft-centric, case-based, or BPMN-orchestrated." + - q: "What is the best AI business process automation platform?" + a: "The right AI business process automation platform depends on the process: Sim fits teams that prioritize agent reasoning, multi-step tool use, human input, model flexibility, and cloud or self-hosted deployment." + - q: "Which company builds custom AI agents to automate business processes?" + a: "Sim provides the open-source AI workspace that internal teams, agencies, and consultants can use to build and deploy custom AI agents that automate real work across business systems." + - q: "Should we use an AI agent platform or Automation Anywhere for process automation?" + a: "Sim fits API-connected processes that require contextual AI reasoning, while Automation Anywhere fits processes dominated by desktop RPA and legacy user interfaces." + - q: "Should we use an AI agent platform or UiPath for process automation?" + a: "Sim fits focused AI agent workflows across existing tools, while UiPath fits enterprises that need a broad automation suite spanning RPA, documents, orchestration, and agentic automation." + - q: "Can AI agents replace RPA bots?" + a: "AI agents can replace some decision and knowledge-work steps, but UiPath, Automation Anywhere, and Power Automate Desktop remain appropriate when a process must operate a legacy interface without an API." + - q: "Can AI agents automate an entire business process?" + a: "Sim can coordinate an end-to-end business process, but enterprises should keep stable policies and high-impact actions deterministic and require human approval where risk warrants it." + - q: "Which platform is best for business processes with human approval steps?" + a: "Sim fits agent-led processes with human approval because its Human in the Loop block pauses a run, collects structured fields, and lets a downstream Condition enforce the submitted decision." + - q: "Which business process automation platform is best for Microsoft 365?" + a: "Microsoft Power Automate fits Microsoft 365 processes because it integrates with Teams, SharePoint, Excel, Outlook, Dynamics 365, Azure, and the wider Power Platform." + - q: "Which business process automation platform is best for IT operations?" + a: "ServiceNow fits IT operations already managed in ServiceNow, while Sim fits cases where an AI agent must coordinate work across several independent IT tools and APIs." + - q: "Which business process automation platform is best for legacy applications?" + a: "UiPath and Automation Anywhere fit legacy applications because both platforms combine enterprise RPA with broader automation and AI capabilities." + - q: "Which business process automation platform is best for BPMN?" + a: "Camunda fits BPMN-led process orchestration because it centers execution on explicit process models spanning services, systems, rules, and people." + - q: "Which business process automation platform is best for case management?" + a: "Pega and Appian fit complex case management, while ServiceNow fits cases involving IT, employee, or customer-service records already managed in ServiceNow." + - q: "Can Sim be self-hosted for enterprise business processes?" + a: "Sim can be self-hosted, with local model support available on any self-hosted deployment; Sim's core is Apache 2.0, while production use of features in apps/sim/ee requires an active Enterprise subscription under the Sim Enterprise License." + - q: "Is Sim open source?" + a: "Sim's core is open source under the OSI-approved Apache License 2.0, while apps/sim/ee is covered by the separate Sim Enterprise License and requires an Enterprise subscription for production use." + - q: "Does Sim support human-in-the-loop approvals?" + a: "Sim supports human-in-the-loop decisions by pausing a workflow and collecting form fields, but a downstream Condition must inspect an approve-or-reject field and route the run accordingly." + - q: "Does Sim support local AI models?" + a: "Sim supports Ollama, vLLM, LM Studio, and LiteLLM on self-hosted deployments without requiring an Enterprise plan." + - q: "How much do AI business process automation platforms cost?" + a: "Sim, UiPath, Automation Anywhere, Microsoft Power Automate, ServiceNow, Appian, Pega, and Camunda use different combinations of subscriptions, users, bots, processes, capacity, model usage, platform products, and enterprise agreements, so buyers should model the complete process rather than compare one advertised entry price." + - q: "What is the difference between business process automation and workflow automation?" + a: "Business process automation usually coordinates a broader operational outcome across people and systems, while workflow automation can describe a narrower sequence of triggers and actions within that process." + - q: "What is the difference between business process automation and agentic automation?" + a: "Business process automation is the broader discipline of coordinating repeatable work, while agentic automation adds AI systems that can interpret context, choose tools, and make bounded decisions inside the process." +--- + +## TL;DR + +AI business process automation platforms combine process orchestration, integrations, human approvals, governance, and AI-assisted decisions across enterprise operations. Sim fits teams that want a flexible agent workspace for multi-step processes, UiPath and Automation Anywhere fit RPA-heavy estates, and Camunda fits BPMN-led process orchestration. + +This comparison covers Sim, UiPath, Automation Anywhere, Microsoft Power Automate, ServiceNow, Appian, Pega, and Camunda. Product capabilities, licensing approaches, and deployment choices were reviewed against vendor sources as of November 2026. + +## What are the top platforms for business process automation with AI? + +The eight platforms solve different combinations of agentic reasoning, RPA, workflow automation, case management, and BPM orchestration. The order below is not a ranking. + +| Platform | Fits | Process focus | Human involvement | +|---|---|---|---| +| Sim | Multi-step AI processes across existing tools | Agentic workflows, API operations, knowledge work, and approvals | Human in the Loop pauses a run; a downstream Condition routes on submitted fields | +| [UiPath](https://www.uipath.com/pricing) | Enterprises combining RPA with agentic automation | UI automation, document processing, and attended or unattended RPA | Human tasks and validation within the UiPath platform | +| [Automation Anywhere](https://www.automationanywhere.com/products/agentic-process-automation-system) | RPA programs adding AI agents | RPA, documents, UI tasks, and agentic processes | People can participate in orchestrated processes | +| [Microsoft Power Automate](https://learn.microsoft.com/en-us/power-automate/) | Microsoft-centric organizations | Cloud flows, desktop flows, approvals, and Microsoft 365 processes | Approval actions and connected interactions | +| [ServiceNow](https://www.servicenow.com/products/ai-agents.html) | IT and employee service operations on ServiceNow | ITSM, customer service, cases, and requests | Approvals and assignments within platform workflows | +| [Appian](https://appian.com/products/platform/overview) | Case management and low-code process applications | Cases, rules, forms, documents, and data-centric applications | User tasks, assignments, forms, and approvals | +| [Pega](https://www.pega.com/products/platform) | Complex case management and decision-heavy operations | Cases, decisioning, and workflow | Assignments, stages, and exception handling | +| [Camunda](https://camunda.com/pricing/) | BPMN-first orchestration across services and people | BPMN processes, service orchestration, human tasks, and long-running processes | User tasks and human task management | + +This comparison emphasizes the ability to coordinate AI reasoning with deterministic actions, human approvals, integrations, governance, and practical deployment choices. An AI agent workspace, an RPA suite, and a BPM suite solve different operating problems. + +## How should an enterprise compare AI business process automation platforms? + +Compare each platform against the process being automated rather than against a generic feature count. + +1. **Where does the work happen?** API-first work favors an agent workspace or orchestration platform, while legacy desktop work may require RPA. +2. **Where is reasoning actually needed?** Use an agent for classification, synthesis, planning, or exception handling. Keep stable policies and calculations deterministic. +3. **How does a person intervene?** Check whether the platform can pause a running process, collect structured input, route the decision, and preserve the result. +4. **What must be governed?** Compare roles, credentials, execution records, audit capabilities, retention controls, and deployment boundaries. +5. **How will the process change?** BPM suites fit formal models and case structures, while visual agent workspaces can suit tool-connected knowledge work. +6. **What drives cost?** User, bot, process, action, compute, model, and enterprise-feature charges behave differently as volume grows. + +For a deeper requirements framework, use the [AI workflow automation platform buyer's checklist](https://www.sim.ai/library/ai-workflow-automation-platform-buyers-checklist). For the distinction between software bots and reasoning systems, see [AI agents vs RPA](https://www.sim.ai/library/ai-agents-vs-rpa). + +## What key facts should buyers know? + +Licensing, deployment, and billing units often reveal more than a long feature list. + +- **Sim:** Sim Cloud has subscription plans, and [self-hosting is supported](https://docs.sim.ai/platform/self-hosting). [Sim's core is Apache 2.0](https://github.com/simstudioai/sim/blob/main/LICENSE), while `apps/sim/ee` is governed by the [separate Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE). Production use of enterprise-licensed features requires an active Sim Enterprise subscription. +- **UiPath:** UiPath documents cloud plans and sales-led enterprise options on its [pricing page](https://www.uipath.com/pricing), alongside an Automation Suite for self-managed deployments. +- **Automation Anywhere:** Automation Anywhere presents an [agentic process automation platform](https://www.automationanywhere.com/products/agentic-process-automation-system) that coordinates AI agents, RPA, APIs, documents, and people. +- **Microsoft Power Automate:** Microsoft documents multiple plan and capacity choices on the [Power Automate pricing page](https://www.microsoft.com/en-us/power-platform/products/power-automate/pricing). +- **ServiceNow:** ServiceNow offers product-specific subscriptions and custom quotes, including [ITSM packages](https://www.servicenow.com/products/itsm/pricing.html) with request, incident, workflow, and AI capabilities. +- **Appian:** Appian describes per-user and per-application platform packaging on its [pricing page](https://appian.com/products/pricing). +- **Pega:** Pega presents its workflow, case, decisioning, and agent orchestration capabilities on the [Pega Platform page](https://www.pega.com/products/platform). +- **Camunda:** Camunda publishes SaaS packaging and sales-led Self-Managed options on its [pricing page](https://camunda.com/pricing/). + +## Is Sim good for AI business process automation? + +Sim fits operations and IT teams that need AI agents to reason inside multi-step processes while APIs, integrations, conditions, and human decisions control execution. + +- **Fits:** Tool-connected knowledge work, AI-assisted exception handling, multi-model agents, structured approvals, and cloud or self-hosted deployment. +- **Commercial model:** As of November 2026, Sim describes cloud plans and enterprise arrangements on its [pricing page](https://www.sim.ai/pricing). Workspace BYOK keys work on every Sim Cloud plan, while organization-level keys require Pro for Teams, Max for Teams, or Enterprise. +- **Capabilities:** Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Its workflow builder combines model calls, tools, integrations, conditions, webhooks, and human input. Agents can connect to external tools through the Model Context Protocol (MCP). [Ollama and OpenAI-compatible local services](https://docs.sim.ai/platform/self-hosting) can be used with self-hosted Sim without requiring Enterprise. +- **Tradeoffs:** Sim is not a desktop RPA suite for automating arbitrary legacy user interfaces. Teams with extensive attended-bot or screen-based automation estates may still need an RPA product. + +## Is UiPath good for AI business process automation? + +UiPath fits large enterprises that want to add agents and AI-assisted decisions to an established RPA and document-processing program. + +- **Fits:** [UiPath deployments](https://www.uipath.com/pricing) that need UI automation, attended or unattended robots, and centralized automation. +- **Commercial model:** As of November 2026, UiPath publishes its available plans and sales-led enterprise tier on the [UiPath pricing page](https://www.uipath.com/pricing). +- **Capabilities:** UiPath combines task and process automation with orchestration, robots, and agentic tooling. This breadth suits processes that cross modern services and older applications requiring user-interface automation. +- **Tradeoffs:** Buyers should map the required products, platform units, and governance components before comparing costs with a focused agent workspace. + +For a replacement-focused comparison, see [UiPath alternatives for AI agents](https://www.sim.ai/library/best-uipath-alternatives-ai-agents-2026). + +## Is Automation Anywhere good for AI business process automation? + +Automation Anywhere fits enterprises that want to extend an RPA program with AI agents, document automation, and centrally managed process execution. + +- **Fits:** [Bot-led operations](https://www.automationanywhere.com/products/agentic-process-automation-system), legacy application work, document processing, shared automation programs, and organizations already invested in Automation Anywhere. +- **Capabilities:** The vendor's [agentic process automation platform](https://www.automationanywhere.com/products/agentic-process-automation-system) combines AI agents, RPA, APIs, documents, orchestration, and human participation. +- **Tradeoffs:** Teams should confirm which platform services, bot capacity, AI services, and deployment requirements are included in an agreement. An RPA-centered operating model may be heavier than necessary for primarily API-based knowledge work. + +## Is Microsoft Power Automate good for AI business process automation? + +Microsoft Power Automate fits organizations that want approvals, cloud flows, and desktop automation within the Microsoft ecosystem. + +- **Fits:** [Microsoft-centric operations](https://learn.microsoft.com/en-us/power-automate/), approval processes, cloud flows, and Windows desktop automation. +- **Commercial model:** As of November 2026, Microsoft lists plan and licensing choices on the [Power Automate pricing page](https://www.microsoft.com/en-us/power-platform/products/power-automate/pricing). +- **Capabilities:** Microsoft's [Power Automate documentation](https://learn.microsoft.com/en-us/power-automate/) covers cloud flows, desktop flows, approvals, connectors, and administration. +- **Tradeoffs:** A solution can span multiple Microsoft products when it combines Power Automate with agent experiences, premium data access, and hosted execution. Model the complete architecture rather than one entry plan. + +## Is ServiceNow good for AI business process automation? + +ServiceNow fits IT, employee, customer-service, and request-fulfillment processes whose records and operating teams already live in ServiceNow. + +- **Fits:** [IT service management](https://www.servicenow.com/products/itsm/pricing.html), case workflows, request fulfillment, incident processes, and ServiceNow-centered operations. +- **Commercial model:** As of November 2026, ServiceNow provides product-specific packages and custom quotes, as shown for [ITSM pricing](https://www.servicenow.com/products/itsm/pricing.html). +- **Capabilities:** ServiceNow combines workflow records, service management, roles, approvals, integrations, and [AI agents](https://www.servicenow.com/products/ai-agents.html) in one enterprise environment. +- **Tradeoffs:** ServiceNow is often a strategic system-of-work decision rather than a focused agent workspace purchase. It may exceed the needs of a team automating a small number of external APIs. + +## Is Appian good for AI business process automation? + +Appian fits enterprises building governed process applications around cases, records, forms, business rules, documents, and human assignments. + +- **Fits:** [Case management and process applications](https://appian.com/products/platform/overview), forms, data-centric work, and processes with many human participants. +- **Commercial model:** As of November 2026, Appian publishes per-user and per-application packaging on its [pricing page](https://appian.com/products/pricing). +- **Capabilities:** Appian positions its platform around process orchestration, case management, data, application development, automation, and AI. +- **Tradeoffs:** Appian can require more application and data-model design than a focused agent workflow. Distinguish the need for an enterprise case application from the need to connect an agent to existing tools. + +## Is Pega good for AI business process automation? + +Pega fits complex case management, customer operations, and decision-heavy processes that require formal stages, rules, assignments, and enterprise controls. + +- **Fits:** [Case management, decisioning, and workflow](https://www.pega.com/products/platform) for complex operational work. +- **Capabilities:** The [Pega Platform](https://www.pega.com/products/platform) brings together agents, workflow, case management, decisioning, and orchestration. +- **Tradeoffs:** Pega adoption can represent a substantial platform and operating-model commitment. A team focused on deploying tool-connected agents may prefer a narrower workspace. + +## Is Camunda good for AI business process automation? + +Camunda fits engineering-led organizations that want BPMN-based orchestration across services, people, rules, and long-running enterprise processes. + +- **Fits:** [BPMN orchestration](https://camunda.com/pricing/), long-running processes, human tasks, complex system coordination, and explicit process models. +- **Commercial model:** As of November 2026, Camunda lists SaaS packages and directs larger or Self-Managed customers to sales on its [pricing page](https://camunda.com/pricing/). +- **Capabilities:** Camunda documents process modeling, orchestration, connectors, user tasks, and operational tooling as parts of its platform. +- **Tradeoffs:** Camunda is an orchestration and process platform rather than a turnkey visual AI agent workspace. Rich agent behavior may require engineering work, custom workers, or additional AI tooling. + +## Should we use Sim or Automation Anywhere for process automation? + +Sim fits processes where AI reasoning and API-connected knowledge work are central. Automation Anywhere fits processes where desktop RPA and an existing bot estate are central. + +Choose Sim when the process must interpret unstructured input, select tools, synthesize information, handle changing context, or request a human decision before acting. Choose Automation Anywhere when the main challenge is operating legacy interfaces, maintaining attended or unattended bots, or extending an established RPA program. + +Many enterprises need both. A deterministic RPA bot can complete a legacy-system transaction after an agent has classified the request, assembled context, and received approval from a person. + +## What does an AI agent process with human approval look like in Sim? + +Sim can coordinate an employee-access request by using an agent to assess the request, pausing for an owner's decision, and routing the submitted result through deterministic conditions. + +1. A webhook or form submits the employee, requested system, requested role, manager, and business justification. +2. Sim retrieves policy and entitlement context from approved systems or knowledge sources. +3. An agent step classifies the request, identifies missing information, summarizes risk, and proposes an access plan. +4. Deterministic conditions route incomplete or prohibited requests without allowing the model to override policy. +5. Sim's Human in the Loop block pauses the run and presents structured fields to the application owner or security reviewer. +6. The reviewer submits an approve-or-reject field and any required comments. +7. A downstream Condition checks that submitted value and sends the run down the approved or rejected branch. +8. The approved branch calls the relevant identity or ticketing API. The rejected branch records the decision and notifies the requester. +9. The workflow stores its run result. Organizations requiring enterprise audit capabilities can evaluate the controls available under the Sim Enterprise License. + +The Human in the Loop block does not approve a request by itself. It pauses and resumes the run with form data, and a downstream Condition must enforce the reviewer's decision. + +## When should an enterprise choose RPA, BPM, or an AI agent workspace? + +Each category fits a different process shape. + +- Choose **RPA** when applications lack usable APIs and the process depends on repeatable desktop or browser interactions. +- Choose a **BPM or case-management suite** when the organization needs formal process models, long-lived cases, structured work queues, and application-like experiences for many participants. +- Choose an **AI agent workspace** when the process includes unstructured information, contextual decisions, model choice, tool use, and integration with existing systems. +- Choose a **suite tied to an incumbent system** when most process data and users already live in that ecosystem. +- Combine categories when an agent must reason about an exception before a governed BPM process or RPA bot completes the transaction. + +## Which platform fits enterprise operations? + +Select Sim when the differentiating requirement is agent reasoning inside a multi-step process with integrations, conditions, model flexibility, and human input. Select UiPath or Automation Anywhere when desktop automation is indispensable. Select Microsoft Power Automate for Microsoft-centric work, ServiceNow for ServiceNow-centered operations, Appian or Pega for case-led enterprise applications, and Camunda for explicit BPMN orchestration. + +Enterprises evaluating security, deployment, and governance requirements can also use the [enterprise AI agent compliance and deployment guide](https://www.sim.ai/library/enterprise-ai-agent-compliance-deployment-guide) and the comparison of [AI agent platforms for enterprise teams](https://www.sim.ai/library/best-ai-agent-platforms-for-enterprise-teams-2026). diff --git a/apps/sim/public/library/best-ai-business-process-automation-platforms-enterprise-operations-2026/cover.jpg b/apps/sim/public/library/best-ai-business-process-automation-platforms-enterprise-operations-2026/cover.jpg new file mode 100644 index 0000000000000000000000000000000000000000..90b09f0e84dad3832bff500327fc015e0671909f GIT binary patch literal 30350 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 QzyJSfK{?&^pB8Wb7yD^Tr~m)} literal 0 HcmV?d00001 From 472399b21fc190886b3eaf73157f33a3f2289f93 Mon Sep 17 00:00:00 2001 From: Waleed Date: Sat, 7 Nov 2026 11:25:01 -0800 Subject: [PATCH 05/29] feat(library): How to Standardize MCP Servers Across an Enterprise: Platforms Compared (#9770) * feat(library): How to Standardize MCP Servers Across an Enterprise: Platforms Compared * Pi Babysit: address PR #9770 feedback --------- Co-authored-by: Sim Pi Agent --- .../index.mdx | 215 ++++++++++++++++++ .../cover.jpg | Bin 0 -> 29556 bytes 2 files changed, 215 insertions(+) create mode 100644 apps/sim/content/library/enterprise-mcp-server-standardization-platforms/index.mdx create mode 100644 apps/sim/public/library/enterprise-mcp-server-standardization-platforms/cover.jpg diff --git a/apps/sim/content/library/enterprise-mcp-server-standardization-platforms/index.mdx b/apps/sim/content/library/enterprise-mcp-server-standardization-platforms/index.mdx new file mode 100644 index 00000000000..658806f0200 --- /dev/null +++ b/apps/sim/content/library/enterprise-mcp-server-standardization-platforms/index.mdx @@ -0,0 +1,215 @@ +--- +slug: enterprise-mcp-server-standardization-platforms +title: 'How to Standardize MCP Servers Across an Enterprise: Platforms Compared' +description: 'Compare enterprise MCP platforms for governance, credentials, observability, self-hosting, and deploying internal workflows as reusable MCP tools.' +seoTitle: 'Enterprise MCP Server Platforms Compared' +date: 2026-11-07 +updated: 2026-11-07 +authors: + - andrew +readingTime: 10 +tags: [MCP, Enterprise AI, AI Agents, Governance, Sim] +ogImage: /library/enterprise-mcp-server-standardization-platforms/cover.jpg +draft: false +faq: + - q: "Our enterprise wants to standardize on MCP servers for connecting internal data to AI agents, what platforms handle this integration reliably at scale?" + a: "Sim fits bidirectional visual MCP workflows, Workato fits integration-led governance, and Microsoft fits enterprises already standardized on Power Platform and Azure." + - q: "How do I connect an MCP server to an AI agent workflow?" + a: "Sim connects an MCP server by storing its authentication as secrets, adding it through an Agent or MCP block, limiting the available tools, testing failure paths, and deploying the workflow with an assigned owner." + - q: "Which AI agent platforms support MCP tool calling?" + a: "Sim, Microsoft Copilot Studio, Microsoft Foundry, OpenAI Agents SDK, Workato, and n8n support MCP tool calling, but their governance, hosting, credential, and server capabilities differ." + - q: "What does an enterprise need to standardize MCP servers across teams?" + a: "An enterprise MCP program needs an approved server inventory, scoped credentials, role-based access, destination controls, execution records, change management, reliability testing, and named owners." + - q: "What is the best enterprise platform for MCP servers?" + a: "Sim fits teams that need visual workflows to both consume MCP tools and become MCP servers, while Workato fits integration-led governance and Microsoft fits Microsoft-native estates." + - q: "Does Sim support MCP clients and MCP servers?" + a: "Sim supports MCP clients through its Agent block and standalone MCP block, and Sim workflows can be deployed as MCP servers for compatible clients." + - q: "Can Sim store MCP credentials in secrets?" + a: "Sim supports secret references in MCP server URLs and headers so credentials do not need to be embedded directly in workflow configuration." + - q: "Can Sim restrict which MCP domains workflows contact?" + a: "Sim supports domain allowlisting to restrict the remote destinations that workflows are permitted to contact." + - q: "Can an internal workflow be exposed as an MCP tool?" + a: "Sim can deploy an internal workflow as an MCP server so compatible agents can call the workflow as a reusable tool." + - q: "Is Sim a Workato enterprise MCP alternative?" + a: "Sim is a Workato enterprise MCP alternative for teams prioritizing visual AI-agent workflows, self-hosting, model choice, and bidirectional MCP support." + - q: "Is n8n open source?" + a: "According to n8n's license documentation, n8n is source-available under the Sustainable Use License rather than open source under an OSI-approved license." + - q: "Is Sim open source?" + a: "Sim's core is open source under Apache 2.0, while code in apps/sim/ee is governed by the separate Sim Enterprise License and requires an Enterprise subscription for production use." + - q: "Can n8n expose a workflow through MCP?" + a: "According to n8n's documentation, its MCP Server Trigger and connected workflow tools can make selected automation capabilities available to MCP clients." + - q: "Is OpenAI Agents SDK enough for enterprise MCP governance?" + a: "OpenAI Agents SDK provides code-first MCP integration and tracing, but the application team remains responsible for hosting, identity, authorization, approvals, reliability, and operational policy." + - q: "Should we use Microsoft Copilot Studio or Microsoft Foundry for MCP?" + a: "Microsoft Copilot Studio fits Power Platform business-agent programs, while Microsoft Foundry fits Azure engineering teams building managed agent applications." + - q: "Should every MCP server have separate production credentials?" + a: "Every production MCP server should use environment-specific credentials scoped to the minimum tools and upstream data required by its approved workflows." + - q: "Who should be allowed to add an MCP server?" + a: "Only authorized platform administrators or builders should register an MCP server, and a separate security or data owner should approve access to sensitive tools." + - q: "What should an MCP tool-call log contain?" + a: "An MCP tool-call log should contain the agent or workflow version, server, tool, runtime identity, timestamp, outcome, latency, retry count, and approval event without exposing secrets." + - q: "Do MCP servers need to be self-hosted?" + a: "MCP servers do not need to be self-hosted, because compatible agents can connect to approved remote servers, but regulated enterprises may choose self-hosting for network, residency, or operational control." + - q: "How do we make MCP integrations reliable at scale?" + a: "An enterprise makes MCP integrations reliable by standardizing ownership, schemas, authentication, timeouts, retry behavior, rate limits, failure paths, monitoring, and rollback procedures." +--- + +## TL;DR + +Sim fits enterprises that want one visual workspace to consume approved MCP servers and deploy internal workflows as MCP servers. Workato fits governance-led integration programs, Microsoft Copilot Studio and Microsoft Foundry fit Microsoft-centered estates, OpenAI Agents SDK fits code-first teams, and n8n fits teams seeking source-available self-hosted automation. + +Model Context Protocol (MCP) gives AI agents a standard way to discover and call external tools. The right choice depends on whether the enterprise prioritizes centralized integration governance, Microsoft-native controls, code-level flexibility, self-hosting, or bidirectional MCP support. For more background, see [What Is an MCP Server?](https://www.sim.ai/library/what-is-an-mcp-server). + +All changing platform and commercial details were checked against the linked first-party sources as of November 2026. + +## What does reliable MCP standardization require at enterprise scale? + +Sim, the open-source AI workspace, fits enterprises that need agents to call approved MCP servers and internal workflows to become reusable MCP tools. Workato fits centralized integration governance, while Microsoft's products fit organizations already centered on Power Platform and Azure. + +A reliable enterprise MCP standard should include: + +1. An approved inventory of MCP servers, owners, data classifications, and permitted tools. +2. Workspace- or project-scoped credentials instead of secrets embedded in prompts or workflow configuration. +3. Permissions governing who can add, modify, approve, and call each server. +4. Domain or network controls that restrict which remote endpoints agents can reach. +5. Execution records for investigating tool calls, errors, latency, and unexpected outputs. +6. Versioning and change controls for server schemas and tool definitions. +7. A supported way to expose approved internal operations as MCP tools. +8. Reliability controls around timeouts, retries, rate limits, and failure handling. + +Protocol compatibility alone is not enterprise standardization. An MCP client can discover and call tools, but the surrounding platform must control credentials, access, operations, and ownership. + +## Which platforms fit different enterprise MCP programs? + +The following comparison is fit-based rather than ranked. Each option puts governance, deployment, and implementation responsibility in a different place. + +| Platform | Enterprise fit | Shared and approved servers | Credentials and permissions | Tool-call visibility | Expose internal workflows through MCP | Deployment model | +|---|---|---|---|---|---|---| +| **Sim** | Teams that want visual agent workflows with inbound and outbound MCP | Workspace-scoped remote servers connect through the Agent block or standalone MCP block | Secret references work in MCP headers, workspace permissions govern registrations, and an operator allowlist can restrict server domains | MCP tool calls are recorded in workflow traces | Yes, deployed workflows can become tools on a workflow MCP server | Sim Cloud or self-hosting | +| **Workato** | Integration programs prioritizing a central MCP control plane | Its [MCP Registry catalogs Workato and third-party servers](https://docs.workato.com/en/mcp/mcp-registry) | The [MCP Gateway authenticates, authorizes, rate-limits, and applies policy](https://docs.workato.com/en/mcp/mcp-gateway) | The gateway captures audit and observability data | [Workato MCP servers expose API recipe collections as tools](https://docs.workato.com/en/mcp/mcp-servers) | Managed commercial service | +| **Microsoft Copilot Studio** | Business-agent programs standardized on Power Platform | [Remote MCP servers can be connected to agents as tools](https://learn.microsoft.com/en-us/microsoft-copilot-studio/mcp-add-existing-server-to-agent) | [Power Platform security and governance controls](https://learn.microsoft.com/en-us/microsoft-copilot-studio/security-and-governance) surround agent access | Copilot Studio and Microsoft governance services provide operational controls | It primarily consumes MCP tools; a separate service is needed to expose other operations | Managed Microsoft service | +| **Microsoft Foundry** | Azure engineering teams building managed agents | [Foundry agents connect to remote MCP endpoints](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/model-context-protocol) | [Azure RBAC supplies project and resource scopes](https://learn.microsoft.com/en-us/azure/foundry/concepts/rbac-foundry) | [Foundry tracing captures agent telemetry](https://learn.microsoft.com/en-us/azure/foundry/observability/how-to/trace-agent-setup) | It acts as an MCP client; teams host a separate MCP service for internal operations | Managed Azure agent runtime | +| **OpenAI Agents SDK** | Engineering teams that want MCP orchestration in application code | The [SDK supports hosted, Streamable HTTP, and local MCP integrations](https://openai.github.io/openai-agents-python/mcp/) | The application owns authentication headers, approval logic, and access policy | [SDK tracing records tool calls and other agent events](https://openai.github.io/openai-agents-python/tracing/) | Developers build or host an MCP server separately | The application team owns hosting and operations | +| **n8n** | Teams wanting self-hosted automation with MCP client and server nodes | The [MCP Client Tool connects agents to external servers](https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.toolmcp) | n8n credentials and project permissions provide the surrounding controls | [Execution-data retention is configurable](https://docs.n8n.io/deploy/host-n8n/configure-n8n/scaling/manage-execution-data) | The [MCP Server Trigger exposes selected tools and workflows](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-langchain.mcptrigger) | n8n Cloud or self-hosting | + +Sim's [external MCP documentation](https://docs.sim.ai/agents/mcp) confirms workspace-scoped registrations, secret references, permissions, tool selection, traces, and the `ALLOWED_MCP_DOMAINS` operator setting. Its [MCP deployment documentation](https://docs.sim.ai/workflows/deployment/mcp) describes how deployed workflows become tools for compatible clients. + +## What are the commercial and licensing facts at a glance? + +As of November 2026, the products use different licensing and billing models: + +- **Sim:** Sim's core is Apache 2.0 and [can be self-hosted](https://docs.sim.ai/platform/self-hosting). Workspace BYOK is available on every Sim Cloud plan according to the [Sim cost documentation](https://docs.sim.ai/platform/costs). Code in `apps/sim/ee` uses the separate [Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE), which requires an active Enterprise subscription for production use. +- **Workato:** Workato is a commercial managed platform. Its [pricing page](https://www.workato.com/pricing) describes Standard, Business MCP, and Enterprise MCP editions, while contract details depend on the customer's agreement. +- **Microsoft Copilot Studio:** Copilot Studio is a proprietary Microsoft cloud service. Microsoft's [licensing guide](https://learn.microsoft.com/en-us/microsoft-copilot-studio/billing-licensing) explains tenant capacity and Copilot Credit consumption. +- **Microsoft Foundry:** Foundry is a proprietary Azure service. Its [pricing page](https://azure.microsoft.com/en-us/pricing/details/microsoft-foundry/) explains that costs depend on the resources and services used. +- **OpenAI Agents SDK:** The [OpenAI Agents SDK uses the MIT License](https://github.com/openai/openai-agents-python/blob/main/LICENSE). Teams separately pay for models and hosted services called by their applications. +- **n8n:** n8n supports Cloud and self-hosted deployments. Its [Sustainable Use License applies to the self-hosted Community edition](https://docs.n8n.io/n8n-community-license/community-license/license-faq), and [n8n Cloud pricing is based on workflow executions](https://n8n.io/pricing/). + +Procurement teams should confirm current terms with each vendor before purchase. + +## How do I connect an MCP server to an AI agent workflow? + +Sim connects an MCP server to an AI agent workflow by storing the endpoint and authentication securely, adding the server through an Agent or MCP block, selecting the permitted tools, and testing the complete workflow before deployment. + +1. **Approve the server and owner.** Record who operates the server, what data it can access, which tools it exposes, and whether its transport is permitted. +2. **Choose the environment boundary.** Decide which Sim workspace or other platform project may use the server and whether development and production require separate endpoints. +3. **Store authentication securely.** Put tokens and other credentials in the platform's secret or connection system instead of a prompt. Sim supports secret references in MCP headers. +4. **Restrict the destination.** On a self-hosted Sim deployment, configure the operator domain allowlist for approved hosts. Treat this as an instance-level control rather than a per-workflow setting. +5. **Add the MCP connection.** In Sim, connect the server through an Agent block when the model should choose tools or through a standalone MCP block when the workflow should make the call explicitly. +6. **Limit available tools.** Expose only the operations required for the workflow rather than granting an agent every capability advertised by the server. +7. **Test normal and failure paths.** Check authentication failures, timeouts, malformed responses, tool errors, and attempts to access unauthorized data. +8. **Review execution records.** Confirm the workflow provides enough evidence to identify the server, tool, result, and failure path required by the organization's audit policy. +9. **Deploy with an owner and rollback plan.** Assign responsibility for credential rotation, server changes, incident response, and disabling the connection. + +Teams evaluating the underlying security model can use [MCP Security: A Practical Guide to Secure MCP Server Development](https://www.sim.ai/library/mcp-security) for threat modeling and controls. + +## How should enterprises manage MCP credentials and secrets across teams? + +Sim should manage MCP credentials through secret references in headers, while each enterprise should separate development and production credentials and scope every credential to the minimum required access. + +A reusable credential standard should specify: + +- Which team owns and rotates each credential. +- Which workspace or environment may use it. +- Which MCP server and tools it authorizes. +- Whether the credential belongs to a service identity or an individual. +- How access is revoked when a server, employee, or workflow changes. +- Whether secrets may cross regional, network, or business-unit boundaries. + +Credentials should never be copied into prompts, workflow descriptions, or shared setup documents. A platform connection is only as safe as the upstream identity and permissions represented by its token. + +## How should enterprises control who can add or call an MCP server? + +An enterprise MCP control plane should separate permission to register a server, permission to approve it, permission to attach it to an agent, and permission to call its individual tools. + +Registration and runtime access are different risks. A platform administrator may approve an endpoint without granting every builder access to every tool, while a workflow can be authorized to read data without being authorized to mutate or delete it. + +At minimum, define four roles: + +1. **Server owner:** Operates the MCP server and its tool definitions. +2. **Security approver:** Reviews identity, data access, network destination, and threat controls. +3. **Workflow builder:** Connects approved tools to an agent or deterministic workflow. +4. **Runtime identity:** Receives only the permissions needed when the workflow executes. + +Sim workspace permissions govern external server registrations. Sim Enterprise access controls can further restrict blocks, tool operations, and MCP deployment, while operator-configured domain allowlisting constrains remote MCP destinations. Tool-level authorization should also be enforced by the MCP server and its upstream systems rather than relying only on the agent platform. + +## How should enterprises log MCP tool calls and failures? + +Enterprise MCP logging should identify the agent, workflow, server, tool, runtime identity, input classification, result, latency, and error without recording secrets or unnecessary sensitive payloads. + +A platform's normal execution history may be enough for debugging but insufficient for regulated audit retention. Buyers should distinguish workflow run records from immutable security audit records and confirm how long each is retained, exported, and searchable. + +The minimum useful event should include: + +- Timestamp and environment. +- Agent or workflow version. +- MCP server and tool name. +- Calling workspace, project, and runtime identity. +- Success, rejection, timeout, or server error. +- Duration and retry count. +- Approval event when a sensitive action requires human review. +- Redacted references to inputs and outputs. + +Do not log bearer tokens, raw credentials, or unrestricted sensitive records merely to make debugging easier. + +## How can an enterprise expose an internal workflow as an MCP server? + +Sim can deploy an approved internal workflow as an MCP server so other compatible agents and clients can call the workflow as a reusable tool. + +This pattern is useful when the enterprise wants to standardize a business operation rather than expose direct access to a database or SaaS API. For example, an MCP tool can invoke an approved customer lookup, procurement check, or account-update workflow that already contains validation and failure handling. + +A safe rollout is: + +1. Define one narrow operation and a stable input schema. +2. Put authorization checks inside the workflow and upstream systems. +3. Return structured, bounded output. +4. Test unauthorized, malformed, duplicate, and high-volume calls. +5. Deploy the Sim workflow as an MCP server. +6. Register the endpoint in the enterprise's approved inventory. +7. Connect it only to permitted agents and environments. +8. Monitor usage and version schema changes deliberately. + +The implementation pattern is covered in [How to Turn a Workflow Into a Reusable MCP Tool (Sim vs n8n)](https://www.sim.ai/library/how-to-turn-a-workflow-into-a-reusable-mcp-tool). + +## Is Sim a Workato enterprise MCP alternative? + +Sim fits teams that prioritize visual AI-agent workflows, self-hosting, an Apache 2.0 core, and the ability to both consume and serve MCP tools. The enterprise exception is governed by the separate Sim Enterprise License linked above. + +Workato fits organizations whose integration center of excellence wants a centrally administered MCP gateway, registry, managed connections, and policy controls. Sim fits teams that want model choice, workflow-level agent orchestration, deployment flexibility, and reusable MCP endpoints without standardizing the whole organization on one proprietary integration suite. + +## Which platform should an enterprise choose for MCP? + +Choose according to the operating model and control plane the organization already supports: + +- Choose **Sim** when builders need MCP tools inside visual agent workflows and need to deploy approved workflows as MCP servers. +- Choose **Workato** when an integration center of excellence owns application connectivity and centralized MCP policy. +- Choose **Microsoft Copilot Studio** when business teams build agents inside Power Platform and Microsoft 365. +- Choose **Microsoft Foundry** when Azure engineers need managed agent infrastructure and Azure-native controls. +- Choose **OpenAI Agents SDK** when developers want to implement MCP policy, authentication, and operations in application code. +- Choose **n8n** when self-hosting and workflow-level MCP client and server nodes matter more than an OSI-approved license. + +For a broader comparison of agent builders rather than enterprise rollout controls, see [AI Agent Builders with MCP Support](https://www.sim.ai/library/best-ai-agent-builders-with-mcp-support). + +## Where can I learn more about enterprise MCP governance? + +Enterprise MCP programs benefit from separating protocol definitions, security, reusable tools, platform selection, and cross-team governance. [Governing AI Agents Built by Different Teams in One Enterprise Workspace](https://www.sim.ai/library/govern-ai-agents-multiple-teams-enterprise-workspace) explains the organizational controls that complement the technical MCP standard. diff --git a/apps/sim/public/library/enterprise-mcp-server-standardization-platforms/cover.jpg b/apps/sim/public/library/enterprise-mcp-server-standardization-platforms/cover.jpg new file mode 100644 index 0000000000000000000000000000000000000000..eabc0670d1012ae6d78e7aec3c2b72521b2c381c GIT binary patch literal 29556 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 literal 0 HcmV?d00001 From 12a65177bba9681aa2912d22f0b0f80e34d961f6 Mon Sep 17 00:00:00 2001 From: Waleed Date: Sat, 7 Nov 2026 11:58:10 -0800 Subject: [PATCH 06/29] fix(execute): derive credential actor from auth type, not body isClientSession (#9771) --- .../api/workflows/[id]/execute/route.test.ts | 57 +++++++++++++++++++ .../app/api/workflows/[id]/execute/route.ts | 11 ++-- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/apps/sim/app/api/workflows/[id]/execute/route.test.ts b/apps/sim/app/api/workflows/[id]/execute/route.test.ts index 775fabceca5..50886847851 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.test.ts @@ -2239,6 +2239,63 @@ describe('workflow execute async route', () => { : executionCall.snapshot expect(snapshot.metadata.enforceCredentialAccess).toBe(true) }) + + describe('credential actor is derived from the authenticated caller', () => { + it.each([ + ['session without isClientSession', EXECUTION_CALLERS[0], true], + ['personal API key', EXECUTION_CALLERS[1], true], + ['workspace API key', EXECUTION_CALLERS[2], false], + ['public API', EXECUTION_CALLERS[3], false], + ['internal JWT', EXECUTION_CALLERS[4], false], + ] as const)('%s: authenticated user is actor = %s', async (_name, caller, expected) => { + configureExecutionCaller(caller) + + const response = await POST( + createCallerExecutionRequest(caller, undefined, 'sync'), + createRouteContext({ id: 'workflow-1' }) + ) + + expect(response.status).toBe(200) + expect(mockPreprocessExecution).toHaveBeenCalledWith( + expect.objectContaining({ useAuthenticatedUserAsActor: expected }) + ) + const executionCall = mockExecuteWorkflowCore.mock.calls[0][0] + const snapshot = + typeof executionCall.snapshot === 'string' + ? JSON.parse(executionCall.snapshot) + : executionCall.snapshot + expect(snapshot.metadata.enforceCredentialAccess).toBe(expected) + }) + + it('authorizes a session run that defaults to draft state at write', async () => { + const response = await POST( + createMockRequest('POST', { input: {} }, { 'Content-Type': 'application/json' }), + createRouteContext({ id: 'workflow-1' }) + ) + + expect(response.status).toBe(200) + expect(mockAuthorizeWorkflowByWorkspacePermission).toHaveBeenCalledWith( + expect.objectContaining({ action: 'write' }) + ) + }) + + it('authorizes a session run of the deployed state at read', async () => { + const response = await POST( + createMockRequest( + 'POST', + { input: {}, useDraftState: false }, + { 'Content-Type': 'application/json' } + ), + createRouteContext({ id: 'workflow-1' }) + ) + + expect(response.status).toBe(200) + expect(mockAuthorizeWorkflowByWorkspacePermission).toHaveBeenCalledWith( + expect.objectContaining({ action: 'read' }) + ) + }) + }) + describe('triggerType override gate', () => { it.each([ ['personal API key', EXECUTION_CALLERS[1]], diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index c801059fcb8..a2c5f27608a 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -945,7 +945,7 @@ async function handleExecutePost( ? false : (useDraftState ?? auth.authType === AuthType.SESSION) const requiresWriteExecutionAccess = Boolean( - useDraftState || workflowStateOverride || rawRunFromBlock + shouldUseDraftState || workflowStateOverride || rawRunFromBlock ) if (req.signal.aborted) { @@ -987,11 +987,14 @@ async function handleExecutePost( } /** - * Interactive sessions and personal keys preserve the authenticated human - * as actor. Preprocessing resolves the workspace payer independently. + * Sessions and personal keys preserve the authenticated human as actor. + * Preprocessing resolves the workspace payer independently. This follows + * the auth type, never the body's `isClientSession`: a session runs the + * draft it can edit, so letting it opt out would run that draft with the + * billing account's credentials. */ const useAuthenticatedUserAsActor = - isClientSession || + auth.authType === AuthType.SESSION || (auth.authType === AuthType.API_KEY && auth.apiKeyType === 'personal') || useMcpBridgeAuthenticatedUserAsActor From e39ae889c723efe4464a145b0160335bbb2b7a1b Mon Sep 17 00:00:00 2001 From: Waleed Date: Sat, 7 Nov 2026 12:05:45 -0800 Subject: [PATCH 07/29] fix(oauth): reject cross-site workspace OAuth launches (#9772) * fix(oauth): reject cross-site workspace OAuth launches * fix(oauth): keep workspace launches out of the signed-in login exception --- apps/sim/app/(auth)/auth-redirect.ts | 12 +++++- .../api/auth/oauth2/authorize/route.test.ts | 41 +++++++++++++++++-- .../app/api/auth/oauth2/authorize/route.ts | 7 ++++ apps/sim/proxy.test.ts | 24 +++++++++++ scripts/check-test-types.baseline.json | 2 +- 5 files changed, 80 insertions(+), 6 deletions(-) diff --git a/apps/sim/app/(auth)/auth-redirect.ts b/apps/sim/app/(auth)/auth-redirect.ts index c487f5a1c99..9edf95449d5 100644 --- a/apps/sim/app/(auth)/auth-redirect.ts +++ b/apps/sim/app/(auth)/auth-redirect.ts @@ -79,12 +79,20 @@ export function resolveAuthRedirect({ redirect, callbackUrl, inviteFlow }: AuthR } } -/** OAuth reauthentication must reach the form even when an older session cookie exists. */ +/** + * OAuth reauthentication must reach the form even when an older session cookie exists. + * Only an OAuth client's request qualifies: a workspace account launch on the same path + * would otherwise turn a cross-site link into a same-origin request after sign-in. + */ export function isOAuthAuthorizationCallback(callbackUrl: string, origin: string): boolean { if (!callbackUrl) return false try { const callback = new URL(callbackUrl, origin) - return callback.origin === origin && callback.pathname === '/api/auth/oauth2/authorize' + return ( + callback.origin === origin && + callback.pathname === '/api/auth/oauth2/authorize' && + callback.searchParams.has('client_id') + ) } catch { return false } diff --git a/apps/sim/app/api/auth/oauth2/authorize/route.test.ts b/apps/sim/app/api/auth/oauth2/authorize/route.test.ts index 93e81c31c7f..791740d9412 100644 --- a/apps/sim/app/api/auth/oauth2/authorize/route.test.ts +++ b/apps/sim/app/api/auth/oauth2/authorize/route.test.ts @@ -8,6 +8,7 @@ import { } from '@sim/testing' import { oauthUtilsMock, oauthUtilsMockFns } from '@sim/testing/mocks/oauth-utils.mock' import { urlsMockFns } from '@sim/testing/mocks/urls.mock' +import type { NextRequest } from 'next/server' import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' import { ForbiddenOperationError } from '@/lib/core/application/forbidden' import { OrchestrationError } from '@/lib/core/orchestration/types' @@ -62,17 +63,19 @@ vi.mock('@/lib/oauth/quickbooks-state', () => ({ createQuickBooksOAuthState: mocks.createQuickBooksState, })) -import { GET } from '@/app/api/auth/oauth2/authorize/route' +import { GET as routeGET } from '@/app/api/auth/oauth2/authorize/route' + +const GET = (req: NextRequest) => routeGET(req, { params: Promise.resolve({}) }) const { mockGetPerRequestOAuthLinkScopes, mockGetCanonicalScopesForProvider } = oauthUtilsMockFns const BASE_URL = 'https://sim.test' const WORKSPACE_ID = '11111111-2222-4333-8444-555555555555' -function request(query: Record) { +function request(query: Record, headers: Record = {}) { const url = new URL('/api/auth/oauth2/authorize', BASE_URL) for (const [key, value] of Object.entries(query)) url.searchParams.set(key, value) - return createMockRequest('GET', undefined, {}, url.toString()) + return createMockRequest('GET', undefined, headers, url.toString()) } function linkResponse(url = 'https://provider.example/authorize') { @@ -439,6 +442,38 @@ describe('OAuth2 authorize route', () => { expect(mocks.betterAuthGET).not.toHaveBeenCalled() }) + it('refuses a cross-site workspace launch before creating a connection draft', async () => { + const response = await GET( + request( + { providerId: 'google-email', workspaceId: WORKSPACE_ID, credentialId: 'credential-1' }, + { 'sec-fetch-site': 'cross-site' } + ) + ) + + expect(response.headers.get('location')).toBe(`${BASE_URL}/home?error=oauth_link_invalid`) + expect(mocks.createConnection).not.toHaveBeenCalled() + expect(mocks.linkAccount).not.toHaveBeenCalled() + }) + + it.each([{ 'sec-fetch-site': 'none' }, { 'sec-fetch-site': 'same-site' }])( + 'starts a workspace launch that is not cross-site: %o', + async (headers) => { + const response = await GET( + request({ providerId: 'google-email', workspaceId: WORKSPACE_ID }, headers) + ) + + expect(mocks.createConnection).toHaveBeenCalledOnce() + expect(response.headers.get('location')).toBe('https://provider.example/authorize') + } + ) + + it('starts a cross-site launch of the signed-in user’s own connection draft', async () => { + const response = await GET(request({ draftId: 'draft-1' }, { 'sec-fetch-site': 'cross-site' })) + + expect(mocks.launchConnection).toHaveBeenCalledOnce() + expect(response.headers.get('location')).toBe('https://provider.example/authorize') + }) + it('requires a configured OAuth client before creating a legacy draft', async () => { mocks.requireClient.mockImplementationOnce(() => { throw new Error('OAuth client is not configured') diff --git a/apps/sim/app/api/auth/oauth2/authorize/route.ts b/apps/sim/app/api/auth/oauth2/authorize/route.ts index 2f3870d1ea9..abd36a647b9 100644 --- a/apps/sim/app/api/auth/oauth2/authorize/route.ts +++ b/apps/sim/app/api/auth/oauth2/authorize/route.ts @@ -22,6 +22,7 @@ import { ForbiddenOperationError } from '@/lib/core/application/forbidden' import { requireConfiguredOAuthClient } from '@/lib/core/config/env-capabilities.server' import { isAuthDisabled } from '@/lib/core/config/env-flags' import { OrchestrationError } from '@/lib/core/orchestration/types' +import { isCrossSiteSessionRequest } from '@/lib/core/security/same-origin' import { getBaseUrl } from '@/lib/core/utils/urls' import { isSameOrigin } from '@/lib/core/utils/validation' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' @@ -257,6 +258,12 @@ export const GET = withRouteHandler(async (request: NextRequest) => { if (!fromConnectionDraft) { if (!workspaceId) throw new Error('Workspace OAuth launch is missing its owner') + // The query alone picks the workspace and credential, so a cross-site page + // must not be able to start this launch with the viewer's session cookie. + if (isCrossSiteSessionRequest(request)) { + logger.warn('Rejected cross-site OAuth workspace launch', { userId, providerId }) + return NextResponse.redirect(`${baseUrl}${APP_ENTRY_PATH}?error=oauth_link_invalid`) + } try { const connection = await createCredentialConnection.execute({ principal, diff --git a/apps/sim/proxy.test.ts b/apps/sim/proxy.test.ts index 59c9f3d2369..0b922053604 100644 --- a/apps/sim/proxy.test.ts +++ b/apps/sim/proxy.test.ts @@ -280,3 +280,27 @@ describe('proxy matcher', () => { expect(matches('/api/internal/file-doc/seed')).toBe(true) }) }) + +describe('signed-in login with an OAuth authorize callback', () => { + function signedInLogin(callbackUrl: string) { + const url = new URL('https://app.sim.test/login') + url.searchParams.set('callbackUrl', callbackUrl) + return new NextRequest(url, { + headers: { cookie: 'better-auth.session_token=session-token.signature' }, + }) + } + + it('shows the form so an OAuth client can reauthenticate its user', async () => { + const response = await proxy( + signedInLogin('/api/auth/oauth2/authorize?client_id=sim-cli&response_type=code') + ) + expect(response.headers.get('location')).toBeNull() + }) + + it('sends a workspace account launch to the app instead of through sign-in', async () => { + const response = await proxy( + signedInLogin('/api/auth/oauth2/authorize?providerId=google-email&workspaceId=workspace-1') + ) + expect(response.headers.get('location')).toBe('https://app.sim.test/home') + }) +}) diff --git a/scripts/check-test-types.baseline.json b/scripts/check-test-types.baseline.json index 18848c29ebb..e24d2c69d44 100644 --- a/scripts/check-test-types.baseline.json +++ b/scripts/check-test-types.baseline.json @@ -30,7 +30,7 @@ }, "apps/sim/app/api/auth/oauth2/authorize/route.test.ts": { "TS2345": 2, - "TS2554": 22 + "TS2554": 1 }, "apps/sim/app/api/auth/oauth2/callback/quickbooks/route.test.ts": { "TS2554": 3 From 83914e7252b0e22eba861523eac95602807fc35c Mon Sep 17 00:00:00 2001 From: Waleed Date: Sat, 7 Nov 2026 12:11:58 -0800 Subject: [PATCH 08/29] chore(execute): tidy credential-actor tests and comment (#9775) --- .../api/workflows/[id]/execute/route.test.ts | 49 ++++++++----------- .../app/api/workflows/[id]/execute/route.ts | 8 ++- 2 files changed, 24 insertions(+), 33 deletions(-) diff --git a/apps/sim/app/api/workflows/[id]/execute/route.test.ts b/apps/sim/app/api/workflows/[id]/execute/route.test.ts index 50886847851..a6ce1ca061d 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.test.ts @@ -2241,13 +2241,16 @@ describe('workflow execute async route', () => { }) describe('credential actor is derived from the authenticated caller', () => { - it.each([ - ['session without isClientSession', EXECUTION_CALLERS[0], true], - ['personal API key', EXECUTION_CALLERS[1], true], - ['workspace API key', EXECUTION_CALLERS[2], false], - ['public API', EXECUTION_CALLERS[3], false], - ['internal JWT', EXECUTION_CALLERS[4], false], - ] as const)('%s: authenticated user is actor = %s', async (_name, caller, expected) => { + const ACTS_AS_AUTHENTICATED_USER: Record = { + session: true, + 'personal API key': true, + 'workspace API key': false, + 'public API': false, + 'internal JWT': false, + } + + it.each(EXECUTION_CALLERS)('$caseName', async (caller) => { + const expected = ACTS_AS_AUTHENTICATED_USER[caller.caseName] configureExecutionCaller(caller) const response = await POST( @@ -2259,31 +2262,21 @@ describe('workflow execute async route', () => { expect(mockPreprocessExecution).toHaveBeenCalledWith( expect.objectContaining({ useAuthenticatedUserAsActor: expected }) ) - const executionCall = mockExecuteWorkflowCore.mock.calls[0][0] - const snapshot = - typeof executionCall.snapshot === 'string' - ? JSON.parse(executionCall.snapshot) - : executionCall.snapshot - expect(snapshot.metadata.enforceCredentialAccess).toBe(expected) - }) - - it('authorizes a session run that defaults to draft state at write', async () => { - const response = await POST( - createMockRequest('POST', { input: {} }, { 'Content-Type': 'application/json' }), - createRouteContext({ id: 'workflow-1' }) - ) - - expect(response.status).toBe(200) - expect(mockAuthorizeWorkflowByWorkspacePermission).toHaveBeenCalledWith( - expect.objectContaining({ action: 'write' }) - ) + expect( + mockExecuteWorkflowCore.mock.calls[0][0].snapshot.metadata.enforceCredentialAccess + ).toBe(expected) }) + }) - it('authorizes a session run of the deployed state at read', async () => { + describe('execution access level', () => { + it.each([ + [{}, 'write'], + [{ useDraftState: false }, 'read'], + ] as const)('session run with %o authorizes at %s', async (bodyExtra, action) => { const response = await POST( createMockRequest( 'POST', - { input: {}, useDraftState: false }, + { input: {}, ...bodyExtra }, { 'Content-Type': 'application/json' } ), createRouteContext({ id: 'workflow-1' }) @@ -2291,7 +2284,7 @@ describe('workflow execute async route', () => { expect(response.status).toBe(200) expect(mockAuthorizeWorkflowByWorkspacePermission).toHaveBeenCalledWith( - expect.objectContaining({ action: 'read' }) + expect.objectContaining({ action }) ) }) }) diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index a2c5f27608a..94d7625a68f 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -987,11 +987,9 @@ async function handleExecutePost( } /** - * Sessions and personal keys preserve the authenticated human as actor. - * Preprocessing resolves the workspace payer independently. This follows - * the auth type, never the body's `isClientSession`: a session runs the - * draft it can edit, so letting it opt out would run that draft with the - * billing account's credentials. + * Sessions and personal keys act as the authenticated human. Keyed on auth + * type, not the body's `isClientSession`, so a session cannot opt out and + * run its editable draft on the billing account's credentials. */ const useAuthenticatedUserAsActor = auth.authType === AuthType.SESSION || From 63487f78231e749e63bfc4183ce6ba9d29535819 Mon Sep 17 00:00:00 2001 From: Waleed Date: Sat, 7 Nov 2026 12:27:37 -0800 Subject: [PATCH 09/29] fix(sandbox): cap task timer bridges and dispose isolates on host timeout (#9776) * fix(sandbox): cap task timer bridges and dispose isolates on host timeout * improvement(sandbox): share timer admission and worker cancel helpers * improvement(sandbox): cover interval reuse in the timer cap e2e check --- apps/sim/lib/execution/isolated-vm-worker.cjs | 38 +++++++++++++++++ apps/sim/lib/execution/isolated-vm.test.ts | 25 ++++++++++- apps/sim/lib/execution/isolated-vm.ts | 20 +++++---- .../scripts/test-isolated-vm-remote-e2e.ts | 41 ++++++++++++++++++- 4 files changed, 115 insertions(+), 9 deletions(-) diff --git a/apps/sim/lib/execution/isolated-vm-worker.cjs b/apps/sim/lib/execution/isolated-vm-worker.cjs index c947ad4abe8..5821120b701 100644 --- a/apps/sim/lib/execution/isolated-vm-worker.cjs +++ b/apps/sim/lib/execution/isolated-vm-worker.cjs @@ -15,6 +15,14 @@ let brokerIdCounter = 0 const FETCH_TIMEOUT_MS = 300000 // 5 minutes const BROKER_TIMEOUT_MS = 300000 const MAX_STDOUT_CHARS = Number.parseInt(process.env.IVM_MAX_STDOUT_CHARS || '', 10) || 200000 +/** + * Task-mode timer bridges keep a Node `Timeout`, a Map entry, and an `ivm.Reference` + * per scheduled timer on the shared worker heap, outside the isolate's `memoryLimit`. + * These ceilings charge a timer flood to the offending execution instead of every + * co-tenant on the worker. + */ +const MAX_PENDING_TIMERS = 1000 +const MAX_ACTIVE_INTERVALS = 50 const MAX_FETCH_OPTIONS_JSON_CHARS = Number.parseInt(process.env.IVM_MAX_FETCH_OPTIONS_JSON_CHARS || '', 10) || 256 * 1024 @@ -757,7 +765,36 @@ async function executeTask(request, executionId) { return v } + // Thrown into the isolate, so the stack must not carry worker host frames. + const timerLimitError = (fnRef, message) => { + try { + fnRef.release() + } catch {} + const error = new RangeError(message) + error.stack = `RangeError: ${message}` + return error + } + const admitTimer = (fnRef, recurring) => { + if (timers.size >= MAX_PENDING_TIMERS) { + throw timerLimitError( + fnRef, + `Too many pending timers (limit ${MAX_PENDING_TIMERS} per execution)` + ) + } + if (!recurring) return + let activeIntervals = 0 + for (const entry of timers.values()) { + if (entry.recurring && ++activeIntervals >= MAX_ACTIVE_INTERVALS) { + throw timerLimitError( + fnRef, + `Too many active intervals (limit ${MAX_ACTIVE_INTERVALS} per execution)` + ) + } + } + } + const setTimeoutRef = new ivm.Reference((fnRef, msRef) => { + admitTimer(fnRef, false) const id = nextTimerId++ const delay = Math.max(0, Math.min(Number(unwrapPrimitive(msRef)) || 0, timeoutMs)) const nodeTimer = setTimeout(() => { @@ -797,6 +834,7 @@ async function executeTask(request, executionId) { await jail.set('__clearTimeoutRef', clearTimeoutRef) const setIntervalRef = new ivm.Reference((fnRef, msRef) => { + admitTimer(fnRef, true) const id = nextTimerId++ const delay = Math.max(1, Math.min(Number(unwrapPrimitive(msRef)) || 1, timeoutMs)) const nodeTimer = setInterval(() => { diff --git a/apps/sim/lib/execution/isolated-vm.test.ts b/apps/sim/lib/execution/isolated-vm.test.ts index af76efff66b..cdac6b125d6 100644 --- a/apps/sim/lib/execution/isolated-vm.test.ts +++ b/apps/sim/lib/execution/isolated-vm.test.ts @@ -87,6 +87,8 @@ type ControllableReadyProc = { spawn: SpawnFactory dispatched: Promise release: (result?: unknown) => void + /** Every message the pool sent the worker, in order. */ + sent: Array<{ type?: string; executionId?: number }> } /** @@ -104,11 +106,13 @@ function createControllableReadyProc(): ControllableReadyProc { }) let proc: MockProc | undefined let lastExecutionId = 0 + const sent: ControllableReadyProc['sent'] = [] const spawn: SpawnFactory = () => { const current = createBaseProc() current.send = (message: unknown) => { const msg = message as { type?: string; executionId?: number } + sent.push(msg) if (msg.type === 'execute') { lastExecutionId = msg.executionId ?? 0 markDispatched() @@ -128,7 +132,7 @@ function createControllableReadyProc(): ControllableReadyProc { }) } - return { spawn, dispatched, release } + return { spawn, dispatched, release, sent } } function createReadyFetchProxyProc(fetchMessage: { url: string; optionsJson?: string }): MockProc { @@ -336,6 +340,25 @@ describe('isolated-vm scheduler', () => { expect(spawnMock).toHaveBeenCalledTimes(2) }) + it('disposes the worker execution when the host timeout fires', async () => { + const { spawn, sent } = createControllableReadyProc() + const { executeInIsolatedVM } = await loadExecutionModule({ spawns: [spawn] }) + + const result = await executeInIsolatedVM({ + code: 'await new Promise(() => {})', + params: {}, + envVars: {}, + contextVariables: {}, + timeoutMs: 10, + requestId: 'req-hung', + }) + + expect(result.termination).toBe('timeout') + const executed = sent.find((message) => message.type === 'execute') + expect(executed?.executionId).toBeDefined() + expect(sent).toContainEqual({ type: 'cancel', executionId: executed?.executionId }) + }) + it('spawns workers with an allowlisted env, never the parent process.env', async () => { const ALLOWED_WORKER_ENV_KEYS = new Set([ 'PATH', diff --git a/apps/sim/lib/execution/isolated-vm.ts b/apps/sim/lib/execution/isolated-vm.ts index b0e6379e169..31a1d6ed253 100644 --- a/apps/sim/lib/execution/isolated-vm.ts +++ b/apps/sim/lib/execution/isolated-vm.ts @@ -1233,6 +1233,15 @@ function spawnWorker(): Promise { * Returns the ready worker with the fewest active executions that still * has capacity, or null if none available. */ +/** Asks the worker to dispose an execution's isolate, sweeping its timers and pending calls. */ +function sendCancel(workerInfo: WorkerInfo, executionId: number) { + try { + workerInfo.process.send({ type: 'cancel', executionId }) + } catch (err) { + logger.warn('Failed to send cancel to worker', { err, workerId: workerInfo.id }) + } +} + function selectWorker(): WorkerInfo | null { let best: WorkerInfo | null = null for (const w of workers.values()) { @@ -1309,6 +1318,9 @@ function dispatchToWorker( const timeout = setTimeout(() => { workerInfo.pendingExecutions.delete(execId) + // A never-settling promise escapes the isolate's own script timeout, so the worker + // would otherwise keep the isolate and its timers alive until the worker retires. + sendCancel(workerInfo, execId) workerInfo.activeExecutions-- totalActiveExecutions-- ownerState.activeExecutions = Math.max(0, ownerState.activeExecutions - 1) @@ -1682,13 +1694,7 @@ export async function executeInIsolatedVM( const wi = workers.get(state.workerId) const pending = wi?.pendingExecutions.get(state.execId) if (pending) pending.cancelled = true - if (wi) { - try { - wi.process.send({ type: 'cancel', executionId: state.execId }) - } catch (err) { - logger.warn('Failed to send cancel to worker', { err, workerId: state.workerId }) - } - } + if (wi) sendCancel(wi, state.execId) } resolveWithRelease({ result: null, diff --git a/apps/sim/scripts/test-isolated-vm-remote-e2e.ts b/apps/sim/scripts/test-isolated-vm-remote-e2e.ts index 06e58e865d8..d09fe1e54d9 100644 --- a/apps/sim/scripts/test-isolated-vm-remote-e2e.ts +++ b/apps/sim/scripts/test-isolated-vm-remote-e2e.ts @@ -9,6 +9,7 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { sleep } from '@sim/utils/helpers' import { generateRandomBytes, generateRandomHex } from '@sim/utils/random' +import type { IsolatedVMExecutionRequest } from '@/lib/execution/isolated-vm' /** * Runs the isolated-vm pool against a real `isolated-vm-host.cjs` over real @@ -383,7 +384,14 @@ async function run() { const slowFetch = (ms: number) => `const r = await fetch('http://127.0.0.1:${slowAddress.port}/?ms=${ms}'); return await r.text()` - const execute = (code: string, extra: { timeoutMs?: number; signal?: AbortSignal } = {}) => + const execute = ( + code: string, + extra: { + timeoutMs?: number + signal?: AbortSignal + task?: IsolatedVMExecutionRequest['task'] + } = {} + ) => executeInIsolatedVM( { code, @@ -393,6 +401,7 @@ async function run() { timeoutMs: extra.timeoutMs ?? 10000, requestId: `e2e-${generateRandomHex(4)}`, ownerKey: 'e2e-owner', + task: extra.task, }, { signal: extra.signal } ) @@ -499,6 +508,36 @@ async function run() { assert.equal(next.result, 'still alive', next.error?.message) }) + const runTask = (code: string) => + execute(code, { + task: { + id: 'e2e-timers', + bundles: [], + bootstrap: '', + brokers: [], + finalize: 'return new Uint8Array([1])', + }, + }) + + await check('caps the timers a task may hold on the shared worker', async () => { + const timeouts = await runTask( + 'const f = () => {}; for (let i = 0; i < 200000; i++) setTimeout(f, 60000)' + ) + assert.match(timeouts.error?.message ?? '', /Too many pending timers/, timeouts.error?.message) + const intervals = await runTask( + 'const f = () => {}; for (let i = 0; i < 200; i++) setInterval(f, 1000)' + ) + assert.match( + intervals.error?.message ?? '', + /Too many active intervals/, + intervals.error?.message + ) + const reused = await runTask( + 'for (let round = 0; round < 3; round++) { const ids = []; for (let i = 0; i < 1000; i++) ids.push(setTimeout(() => {}, 60000)); ids.forEach(clearTimeout); const intervals = []; for (let i = 0; i < 50; i++) intervals.push(setInterval(() => {}, 60000)); intervals.forEach(clearInterval) }' + ) + assert.equal(reused.error, undefined, reused.error?.message) + }) + await check('rejects a connection with the wrong token', async () => { const ws = await openSocket(probeHost, authFrame(generateRandomHex(32))) assert.equal(await closeCode(ws), 4401) From 17a028779c5a4e0ed250b9e0fad84f437deb7933 Mon Sep 17 00:00:00 2001 From: Waleed Date: Sat, 7 Nov 2026 12:30:57 -0800 Subject: [PATCH 10/29] improvement(search): send each Search MCP result fact once (#9778) --- apps/sim/lib/knowledge/mcp/server.test.ts | 70 +++++++++++++++++++++++ apps/sim/lib/knowledge/mcp/server.ts | 59 +++++++++++++++---- 2 files changed, 119 insertions(+), 10 deletions(-) diff --git a/apps/sim/lib/knowledge/mcp/server.test.ts b/apps/sim/lib/knowledge/mcp/server.test.ts index f5bafa7d108..579a6aa317a 100644 --- a/apps/sim/lib/knowledge/mcp/server.test.ts +++ b/apps/sim/lib/knowledge/mcp/server.test.ts @@ -163,6 +163,76 @@ describe('live organization Search MCP', () => { ) }) + const textOf = (result: CallToolResult) => { + const [part] = result.content + if (part?.type !== 'text') throw new Error('Expected a text result') + return JSON.parse(part.text) as Record & { results?: unknown[] } + } + + it('returns each search fact once and keeps facts that differ', async () => { + create() + const message = { + documentId: 'live:message', + documentName: '#release', + sourceUrl: 'https://example.slack.com/archives/C1/p1', + sourceModifiedAt: '2026-09-22T10:00:00.000Z', + sourceDate: '2026-09-22T10:00:00Z', + sourceContainerName: '#release', + sourceContainerUrl: 'https://example.slack.com/archives/C1', + connectorType: 'slack', + content: 'Shipping Friday', + chunkIndex: 0, + } + const event = { + documentId: 'live:event', + documentName: 'Launch review', + sourceUrl: 'https://calendar.example.com/event', + sourceModifiedAt: '2026-09-20T08:00:00.000Z', + sourceDate: '2026-09-25T16:00:00Z', + sourceContainerName: 'Team calendar', + connectorType: 'google_calendar', + content: 'Launch review', + chunkIndex: 0, + } + mocks.liveSearch.mockResolvedValueOnce({ + results: [message, event], + live: coverage, + retrieval: { status: 'complete' }, + }) + const { results } = textOf(await call('search', { query: 'launch' })) + expect(results).toEqual([ + { + documentId: 'live:message', + title: '#release', + citationId: expect.any(String), + citationUrl: 'https://example.slack.com/archives/C1/p1', + sourceDate: '2026-09-22T10:00:00Z', + sourceContainerUrl: 'https://example.slack.com/archives/C1', + connectorType: 'slack', + content: 'Shipping Friday', + }, + { + documentId: 'live:event', + title: 'Launch review', + citationId: expect.any(String), + citationUrl: 'https://calendar.example.com/event', + sourceModifiedAt: '2026-09-20T08:00:00.000Z', + sourceDate: '2026-09-25T16:00:00Z', + sourceContainerName: 'Team calendar', + connectorType: 'google_calendar', + content: 'Launch review', + }, + ]) + }) + + it('returns a read document title and link once', async () => { + create() + const read = textOf(await call('read_document', { documentId })) + expect(read).toMatchObject({ title: 'Release notes', citationUrl: 'https://example.com/notes' }) + expect(read).not.toHaveProperty('documentName') + expect(read).not.toHaveProperty('sourceUrl') + }) + it.each(['search', 'read_document'])( 'refuses live %s content with incomplete secret provenance', async (tool) => { diff --git a/apps/sim/lib/knowledge/mcp/server.ts b/apps/sim/lib/knowledge/mcp/server.ts index 3edfc867937..b965738afc9 100644 --- a/apps/sim/lib/knowledge/mcp/server.ts +++ b/apps/sim/lib/knowledge/mcp/server.ts @@ -1,4 +1,5 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js' import { resolvePrincipalSubjectUserId } from '@sim/auth/principal' import { createLogger } from '@sim/logger' import { isPlainRecord } from '@sim/utils/object' @@ -47,17 +48,58 @@ async function callerErrorMessage(error: unknown): Promise { : undefined } +interface SourceRow { + documentName: string | null + sourceUrl: string | null + sourceModifiedAt?: string | null + sourceDate?: string | null + sourceContainerName?: string +} + +/** + * A document's title, link and dates, each sent once: every turn of the client's conversation + * re-reads them. `sourceModifiedAt` repeats `sourceDate` except where the date is an event start, + * and a Slack message is titled by its channel, so each is kept only when it says something else. + */ +function sourceFacts(row: SourceRow) { + const { documentName: title, sourceModifiedAt, sourceDate, sourceContainerName } = row + const modifiedDiffers = + sourceModifiedAt && (!sourceDate || Date.parse(sourceModifiedAt) !== Date.parse(sourceDate)) + return { + title, + citationUrl: row.sourceUrl, + ...(modifiedDiffers ? { sourceModifiedAt } : {}), + sourceDate: sourceDate ?? null, + ...(sourceContainerName && sourceContainerName !== title ? { sourceContainerName } : {}), + } +} + /** One read_document result in the shape the tool returns it. */ function projectReadResult(result: Awaited>) { - const { knowledgeBaseId: _knowledgeBaseId, knowledgeBaseName: _name, ...document } = result + const { + knowledgeBaseId: _knowledgeBaseId, + knowledgeBaseName: _name, + documentName: _documentName, + sourceUrl: _sourceUrl, + sourceModifiedAt: _sourceModifiedAt, + sourceContainerName: _sourceContainerName, + ...document + } = result return { ...document, - title: result.documentName, citationId: liveCitationId(result.documentId), - citationUrl: result.sourceUrl, + ...sourceFacts(result), } } +/** Characters of text a tool result hands the client. */ +function resultCharacters(result: CallToolResult): number { + return result.content.reduce( + (total, part) => total + (part.type === 'text' ? part.text.length : 0), + 0 + ) +} + interface KnowledgeMcpContext { organizationId: string request: NextRequest @@ -74,6 +116,7 @@ export function createKnowledgeMcpServer(context: KnowledgeMcpContext): McpServe const startedAt = performance.now() const signal = AbortSignal.any([request.signal, toolSignal]) let outcome: SearchMcpActivityInput['outcome'] = 'error' + let resultChars: number | undefined try { signal.throwIfAborted() const limited = await v2RateLimits.publicApi.enforce(request, auth, operation, cost) @@ -92,6 +135,7 @@ export function createKnowledgeMcpServer(context: KnowledgeMcpContext): McpServe } const result = await run(new ResolvedSecretTraceRegistry(), signal) outcome = signal.aborted ? 'cancelled' : result.isError ? 'error' : 'success' + resultChars = resultCharacters(result) return result } catch (error) { if (signal.aborted) outcome = 'cancelled' @@ -118,6 +162,7 @@ export function createKnowledgeMcpServer(context: KnowledgeMcpContext): McpServe userId: activity.userId, outcome, durationMs: activity.durationMs, + resultChars, }) afterResponse(() => recordOrganizationSearchMcpActivity(activity)) } @@ -154,18 +199,12 @@ export function createKnowledgeMcpServer(context: KnowledgeMcpContext): McpServe { results: result.results.map((row) => ({ documentId: row.documentId, - title: row.documentName, - sourceUrl: row.sourceUrl, citationId: liveCitationId(row.documentId), - citationUrl: row.sourceUrl, - sourceModifiedAt: row.sourceModifiedAt ?? null, - sourceDate: row.sourceDate, - sourceContainerName: row.sourceContainerName, + ...sourceFacts(row), sourceContainerUrl: row.sourceContainerUrl, ...(row.queryIndexes ? { queryIndexes: row.queryIndexes } : {}), connectorType: row.connectorType, content: row.content, - chunkIndex: row.chunkIndex, })), retrieval: result.retrieval, live: result.live, From 44ce671d68f306024f0c82fddb335607f379db0a Mon Sep 17 00:00:00 2001 From: Waleed Date: Sat, 7 Nov 2026 12:34:27 -0800 Subject: [PATCH 11/29] fix(file-parsers): reject ZIP archives JSZip reads from outside the guarded region (#9773) --- apps/sim/lib/file-parsers/zip-guard.test.ts | 47 ++++++ apps/sim/lib/file-parsers/zip-guard.ts | 150 ++++++++++++++++++-- 2 files changed, 188 insertions(+), 9 deletions(-) diff --git a/apps/sim/lib/file-parsers/zip-guard.test.ts b/apps/sim/lib/file-parsers/zip-guard.test.ts index d1d3d860839..329de10db7c 100644 --- a/apps/sim/lib/file-parsers/zip-guard.test.ts +++ b/apps/sim/lib/file-parsers/zip-guard.test.ts @@ -322,4 +322,51 @@ describe('assertOoxmlArchiveWithinLimits', () => { const plaintext = Buffer.from('this is just plain text, not a zip archive at all') expect(() => assertOoxmlArchiveWithinLimits(plaintext)).not.toThrow() }) + + describe('archives JSZip reads from a different place than the guard inspects', () => { + const PER_ENTRY_LIMITS: OoxmlSizeLimits = { ...HIGH_LIMITS, maxEntryUncompressedBytes: 100_000 } + const oversizedEntry = () => buildZip({ 'word/document.xml': 'A'.repeat(200_000) }) + + async function expectJsZipReadsOversizedEntry(buffer: Buffer) { + const zip = await JSZip.loadAsync(buffer) + expect(await zip.file('word/document.xml')?.async('string')).toHaveLength(200_000) + } + + it.each([ + ['prepended bytes', Buffer.from('JUNKJUNK'), Buffer.alloc(0)], + ['prepended and trailing bytes', Buffer.from('JUNKJUNK'), Buffer.from('TRAILER')], + ])('rejects an archive with %s that JSZip still opens', async (_, prefix, suffix) => { + const wrapped = Buffer.concat([prefix, await oversizedEntry(), suffix]) + await expectJsZipReadsOversizedEntry(wrapped) + + expect(() => assertOoxmlArchiveWithinLimits(wrapped, PER_ENTRY_LIMITS)).toThrow( + ArchiveIntegrityError + ) + }) + + it('treats any saturated EOCD field as ZIP64, as JSZip does', async () => { + const buffer = await buildZip({ 'word/document.xml': 'hello' }) + buffer.writeUInt16LE(0xffff, buffer.length - 22 + 8) // entries on this disk + await expect(JSZip.loadAsync(buffer)).rejects.toThrow(/ZIP64/) + + expect(() => assertOoxmlArchiveWithinLimits(buffer, HIGH_LIMITS)).toThrow( + ArchiveIntegrityError + ) + }) + + it.each([ + ['an EOCD signature', 0], + ['an EOCD signature with a saturated ZIP64 field', 0xffff], + ])('still no-ops for a non-ZIP buffer that merely contains %s', async (_, entryCount) => { + const eocd = buildCentralDirectoryOnly(0) + eocd.writeUInt16LE(entryCount, 10) + const buffer = Buffer.concat([ + Buffer.from('%PDF-1.7 binary body '), + eocd, + Buffer.from('tail'), + ]) + + expect(() => assertOoxmlArchiveWithinLimits(buffer)).not.toThrow() + }) + }) }) diff --git a/apps/sim/lib/file-parsers/zip-guard.ts b/apps/sim/lib/file-parsers/zip-guard.ts index b77ff6034bf..e31f448e949 100644 --- a/apps/sim/lib/file-parsers/zip-guard.ts +++ b/apps/sim/lib/file-parsers/zip-guard.ts @@ -113,6 +113,127 @@ export function findEocdOffset(buffer: Buffer): number { return -1 } +function signatureBytes(signature: number): Buffer { + const bytes = Buffer.alloc(4) + bytes.writeUInt32LE(signature) + return bytes +} + +const EOCD_SIGNATURE_BYTES = signatureBytes(EOCD_SIGNATURE) +const ZIP64_EOCD_LOCATOR_SIGNATURE_BYTES = signatureBytes(ZIP64_EOCD_LOCATOR_SIGNATURE) +const ZIP64_EOCD_SIGNATURE_BYTES = signatureBytes(ZIP64_EOCD_SIGNATURE) + +function hasSignatureAt(buffer: Buffer, offset: number, signature: number): boolean { + return offset >= 0 && offset + 4 <= buffer.length && buffer.readUInt32LE(offset) === signature +} + +/** + * Whether the EOCD defers to the ZIP64 record. JSZip (and the spec) switch on + * ANY saturated field, not just the count and offset — so must the guard, or a + * lone saturated disk field sends JSZip down a chain the guard never follows. + */ +function eocdUsesZip64(buffer: Buffer, eocdOffset: number): boolean { + return ( + buffer.readUInt16LE(eocdOffset + 4) === UINT16_SENTINEL || + buffer.readUInt16LE(eocdOffset + 6) === UINT16_SENTINEL || + buffer.readUInt16LE(eocdOffset + 8) === UINT16_SENTINEL || + buffer.readUInt16LE(eocdOffset + 10) === UINT16_SENTINEL || + buffer.readUInt32LE(eocdOffset + 12) === UINT32_SENTINEL || + buffer.readUInt32LE(eocdOffset + 16) === UINT32_SENTINEL + ) +} + +interface ZipParserView { + eocdOffset: number + /** Bytes JSZip treats as prepended data and rebases every offset past. */ + baseOffset: number + /** Absolute offset JSZip starts reading central-directory records from. */ + centralDirectoryStart: number +} + +/** + * Where JSZip — which mammoth and the Word document helpers parse through — + * reads the archive from, mirroring `readEndOfCentral` in jszip 3.10. Unlike + * the guard it does not anchor on the buffer tail: it takes the LAST EOCD + * signature anywhere, the last ZIP64 locator signature anywhere (and the last + * ZIP64 record signature if the locator's in-range pointer misses), and treats + * any gap before the central directory as prepended data. Returns `null` when + * JSZip would refuse the buffer. + * + * Fields are read with JSZip's integer semantics, not the spec's: its + * `readInt` folds 4- and 8-byte values through 32-bit shifts, keeping only the + * low dword as a signed int (`readInt32LE`). Reading the full width here would + * let a value with high bits set look out of range while JSZip reads it as an + * ordinary offset or size. + */ +function locateAsZipParser(buffer: Buffer): ZipParserView | null { + const eocdOffset = buffer.lastIndexOf(EOCD_SIGNATURE_BYTES) + if (eocdOffset < 0 || eocdOffset + EOCD_MIN_SIZE > buffer.length) { + return null + } + + let centralDirectoryOffset = buffer.readInt32LE(eocdOffset + 16) + let expectedEocdOffset = centralDirectoryOffset + buffer.readInt32LE(eocdOffset + 12) + if (eocdUsesZip64(buffer, eocdOffset)) { + const locatorOffset = buffer.lastIndexOf(ZIP64_EOCD_LOCATOR_SIGNATURE_BYTES) + if (locatorOffset < 0 || locatorOffset + ZIP64_EOCD_LOCATOR_SIZE > buffer.length) { + return null + } + // JSZip's signature probe throws on an out-of-range pointer instead of + // falling back to the signature scan. + const pointedRecordOffset = buffer.readInt32LE(locatorOffset + 8) + if (pointedRecordOffset < 0 || pointedRecordOffset + 4 > buffer.length) { + return null + } + const recordOffset = hasSignatureAt(buffer, pointedRecordOffset, ZIP64_EOCD_SIGNATURE) + ? pointedRecordOffset + : buffer.lastIndexOf(ZIP64_EOCD_SIGNATURE_BYTES) + if (recordOffset < 0 || recordOffset + 56 > buffer.length) { + return null + } + centralDirectoryOffset = buffer.readInt32LE(recordOffset + 48) + // The record's size field (+4) excludes its leading signature and itself (12 bytes). + expectedEocdOffset = + centralDirectoryOffset + + buffer.readInt32LE(recordOffset + 40) + + 12 + + buffer.readInt32LE(recordOffset + 4) + + ZIP64_EOCD_LOCATOR_SIZE + } + + const baseOffset = eocdOffset - expectedEocdOffset + if (baseOffset < 0 || centralDirectoryOffset < 0) { + return null + } + return { eocdOffset, baseOffset, centralDirectoryStart: baseOffset + centralDirectoryOffset } +} + +/** + * Whether JSZip would read central-directory records from a different place + * than the guard inspects — prepended data, a later EOCD signature, or a + * diverging ZIP64 chain — so a buffer the guard reads as "not a ZIP" (or as a + * different ZIP) would still be inflated in full downstream. A buffer JSZip + * would refuse, or in which it finds no records, is not flagged, so a non-ZIP + * file that merely contains EOCD-like bytes keeps the no-op path. + */ +function zipParserReadsDifferentArchive(buffer: Buffer): boolean { + const parser = locateAsZipParser(buffer) + if ( + !parser || + !hasSignatureAt(buffer, parser.centralDirectoryStart, CENTRAL_DIRECTORY_HEADER_SIGNATURE) + ) { + return false + } + + const guardEocdOffset = findEocdOffset(buffer) + const guardLocation = guardEocdOffset < 0 ? null : locateCentralDirectory(buffer, guardEocdOffset) + return !( + parser.baseOffset === 0 && + parser.eocdOffset === guardEocdOffset && + guardLocation?.offset === parser.centralDirectoryStart + ) +} + export interface CentralDirectoryLocation { offset: number entryCount: number @@ -120,7 +241,7 @@ export interface CentralDirectoryLocation { /** * Resolve the central directory offset and entry count, following the ZIP64 - * end-of-central-directory chain when the 32-bit fields are saturated. + * end-of-central-directory chain when any EOCD field is saturated. */ export function locateCentralDirectory( buffer: Buffer, @@ -129,18 +250,16 @@ export function locateCentralDirectory( let entryCount = buffer.readUInt16LE(eocdOffset + 10) let cdOffset = buffer.readUInt32LE(eocdOffset + 16) - const needsZip64 = entryCount === UINT16_SENTINEL || cdOffset === UINT32_SENTINEL - if (needsZip64) { + if (eocdUsesZip64(buffer, eocdOffset)) { const locatorOffset = eocdOffset - ZIP64_EOCD_LOCATOR_SIZE - if (locatorOffset < 0 || buffer.readUInt32LE(locatorOffset) !== ZIP64_EOCD_LOCATOR_SIGNATURE) { + if (!hasSignatureAt(buffer, locatorOffset, ZIP64_EOCD_LOCATOR_SIGNATURE)) { return null } const zip64EocdOffset = Number(buffer.readBigUInt64LE(locatorOffset + 8)) if ( - zip64EocdOffset < 0 || zip64EocdOffset + 56 > buffer.length || - buffer.readUInt32LE(zip64EocdOffset) !== ZIP64_EOCD_SIGNATURE + !hasSignatureAt(buffer, zip64EocdOffset, ZIP64_EOCD_SIGNATURE) ) { return null } @@ -448,10 +567,11 @@ export interface ZipCentralDirectoryStats { * per — a lied count can neither hide records nor inflate the tally. Unlike a * raw whole-buffer signature scan, STORED entry payloads (e.g. a nested `.zip` * archived without recompression) are never miscounted as records. Returns - * `null` when the buffer is not a parseable ZIP, so callers can fail closed. + * `null` when the buffer is not a parseable ZIP, or when JSZip would read its + * records from somewhere else, so callers can fail closed. */ export function readZipCentralDirectoryStats(buffer: Buffer): ZipCentralDirectoryStats | null { - if (buffer.length < EOCD_MIN_SIZE) { + if (buffer.length < EOCD_MIN_SIZE || zipParserReadsDifferentArchive(buffer)) { return null } @@ -503,12 +623,24 @@ export function readZipCentralDirectoryStats(buffer: Buffer): ZipCentralDirector * or whose entries cannot be verified, is rejected rather than passed through, * so a malformed archive that a downstream library still inflates cannot bypass * the guard. Genuinely non-ZIP inputs (legacy OLE `.xls`/`.doc`, misidentified - * plaintext) no-op and defer to the downstream parser's own validation. + * plaintext) no-op and defer to the downstream parser's own validation. "Genuinely + * non-ZIP" is judged by what JSZip would open, not by the leading bytes: an + * archive with prepended data or a relocated EOCD is rejected, because JSZip + * reads it from a place this guard never inspected. */ export function assertOoxmlArchiveWithinLimits( buffer: Buffer, limits: OoxmlSizeLimits = DEFAULT_OOXML_SIZE_LIMITS ): void { + if (zipParserReadsDifferentArchive(buffer)) { + logger.warn('Rejected ZIP archive: parser would read records the guard cannot inspect', { + compressedBytes: buffer.length, + }) + throw new ArchiveIntegrityError( + 'Archive has prepended data or a relocated end-of-central-directory record; refusing to parse an unverifiable ZIP archive' + ) + } + const declared = sumDeclaredUncompressedSize(buffer, limits) if (declared === null) { if (isZipShaped(buffer)) { From d5451f6087a2e2f2788c692eff0adca9ccc762ec Mon Sep 17 00:00:00 2001 From: Waleed Date: Sat, 7 Nov 2026 12:34:36 -0800 Subject: [PATCH 12/29] fix(desktop): explain macOS Local Network blocks for private self-hosted servers (#9777) * fix(desktop): explain macOS Local Network blocks for private self-hosted servers * fix(desktop): only show Local Network guidance on macOS 15+ --- apps/desktop/electron-builder.yml | 3 +++ apps/desktop/src/main/load-health.test.ts | 5 ++++ apps/desktop/src/main/load-health.ts | 30 +++++++++++++++++++-- apps/desktop/src/renderer/offline/index.tsx | 5 ++++ 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/apps/desktop/electron-builder.yml b/apps/desktop/electron-builder.yml index a52cc14977e..5b303ac360b 100644 --- a/apps/desktop/electron-builder.yml +++ b/apps/desktop/electron-builder.yml @@ -72,6 +72,9 @@ mac: NSScreenCaptureUsageDescription: Mothership captures approved app windows to understand and verify computer-use actions. NSMicrophoneUsageDescription: Sim uses your microphone for voice input in Chat and for meetings you join in the built-in browser. NSCameraUsageDescription: Sim uses your camera for meetings you join in the built-in browser, such as Google Meet. + # macOS 15 blocks connections to private addresses (self-hosted servers on a + # LAN or VPN) until the user allows Sim; this string explains that prompt. + NSLocalNetworkUsageDescription: Sim connects to your self-hosted Sim server and to local addresses you open in the built-in browser. entitlements: build/entitlements.mac.plist entitlementsInherit: build/entitlements.mac.plist notarize: true diff --git a/apps/desktop/src/main/load-health.test.ts b/apps/desktop/src/main/load-health.test.ts index aab1da96f78..e547daa4836 100644 --- a/apps/desktop/src/main/load-health.test.ts +++ b/apps/desktop/src/main/load-health.test.ts @@ -7,6 +7,11 @@ describe('classifyLoadError', () => { expect(classifyLoadError(-2)).toBe('unreachable') expect(classifyLoadError(-1)).toBe('unreachable') }) + + it('reports ERR_ADDRESS_UNREACHABLE as a local-network block only under Local Network privacy', () => { + expect(classifyLoadError(-109, true)).toBe('local_network') + expect(classifyLoadError(-109, false)).toBe('unreachable') + }) }) vi.mock('electron', () => import('@/test/electron-mock')) diff --git a/apps/desktop/src/main/load-health.ts b/apps/desktop/src/main/load-health.ts index de71f85ecb9..56c73d89f45 100644 --- a/apps/desktop/src/main/load-health.ts +++ b/apps/desktop/src/main/load-health.ts @@ -1,3 +1,4 @@ +import { release } from 'node:os' import { createLogger } from '@sim/logger' import type { BrowserWindow } from 'electron' import { type EventRecorder, scrubUrl } from '@/main/observability' @@ -6,14 +7,33 @@ const logger = createLogger('DesktopLoadHealth') const AUTO_RETRY_INTERVAL_MS = 5000 const LOAD_WATCHDOG_MS = 30_000 +/** Darwin 24 is macOS 15, the first release that gates apps behind Local Network privacy. */ +const FIRST_LOCAL_NETWORK_PRIVACY_DARWIN = 24 -export type LoadErrorKind = 'offline' | 'dns' | 'tls' | 'timeout' | 'unreachable' | 'ignored' +function hasLocalNetworkPrivacy(): boolean { + return ( + process.platform === 'darwin' && + Number.parseInt(release(), 10) >= FIRST_LOCAL_NETWORK_PRIVACY_DARWIN + ) +} + +export type LoadErrorKind = + | 'offline' + | 'dns' + | 'tls' + | 'timeout' + | 'local_network' + | 'unreachable' + | 'ignored' /** * Maps Chromium net error codes to recovery copy. -3 (ERR_ABORTED) is emitted * constantly by OAuth redirect chains and in-app aborts and must be ignored. */ -export function classifyLoadError(errorCode: number): LoadErrorKind { +export function classifyLoadError( + errorCode: number, + localNetworkPrivacy: boolean = hasLocalNetworkPrivacy() +): LoadErrorKind { // Only ERR_ABORTED (-3) and success (0) are ignored. ERR_FAILED (-2) and // ERR_IO_PENDING (-1) are real failures that must surface the offline page. if (errorCode === 0 || errorCode === -3) { @@ -31,6 +51,12 @@ export function classifyLoadError(errorCode: number): LoadErrorKind { if (errorCode <= -200 && errorCode >= -213) { return 'tls' } + // macOS Local Network privacy fails the socket with EHOSTUNREACH, which + // Chromium reports as ERR_ADDRESS_UNREACHABLE (-109), so a self-hosted server + // on a private address looks like a routing failure until Sim is allowed. + if (errorCode === -109 && localNetworkPrivacy) { + return 'local_network' + } return 'unreachable' } diff --git a/apps/desktop/src/renderer/offline/index.tsx b/apps/desktop/src/renderer/offline/index.tsx index 45141ccba49..2b507f46e3b 100644 --- a/apps/desktop/src/renderer/offline/index.tsx +++ b/apps/desktop/src/renderer/offline/index.tsx @@ -30,6 +30,11 @@ const ERROR_COPY = { title: 'The server isn’t responding', message: 'The connection timed out. The server may be down or your network may be blocking it.', }, + local_network: { + title: 'Can’t reach the server', + message: + 'If your Sim server is on a local or private network, macOS may be blocking Sim. Turn on Sim in System Settings > Privacy & Security > Local Network, then try again.', + }, unreachable: { title: 'Can’t connect to Sim', message: 'Sim couldn’t reach the server. Check your internet connection, then try again.', From 13c59246bfaa55ea6ec239d6fdc37b08878acea0 Mon Sep 17 00:00:00 2001 From: Waleed Date: Sat, 7 Nov 2026 12:47:33 -0800 Subject: [PATCH 13/29] docs(zoom): clarify account selection and separate search connections (#9779) --- apps/docs/content/docs/integrations/zoom.mdx | 19 ++++++++++++++++++- apps/docs/content/docs/search/zoom.mdx | 4 ++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/apps/docs/content/docs/integrations/zoom.mdx b/apps/docs/content/docs/integrations/zoom.mdx index bdc7f802c83..316e6e41fb3 100644 --- a/apps/docs/content/docs/integrations/zoom.mdx +++ b/apps/docs/content/docs/integrations/zoom.mdx @@ -12,7 +12,24 @@ import { ReferenceDetails } from "@/components/ui/reference-details" {/* MANUAL-CONTENT-START:intro */} Use [Zoom](https://zoom.us/) to manage meetings, retrieve invitations and recordings, and list past participants. Meeting and recording webhooks can start workflows. -Connect a Zoom account from the block or **Integrations** in your workspace sidebar. For account-level authentication, follow the [Server-to-Server OAuth setup](/integrations/zoom-service-account). Manage or disconnect the saved credential from **Integrations**. +### Connect your account + +Connect from a Zoom block or from **Integrations** in your workspace. For an account-level connection, use [Server-to-Server OAuth](/integrations/zoom-service-account). + +1. Open **Integrations → Zoom** in your workspace and select **Add to Sim → Connect with OAuth**. +2. Give the connection a name you will recognize in the workflow's **Zoom Account** picker. +3. On Zoom's authorization page, check the signed-in account. Select **Switch account** if needed, then authorize Sim. +4. Return to your workflow and select that saved connection in the Zoom block. For a read-only check, choose **List Meetings** with user ID `me`. + +The picker lists saved connection names. If you have several Zoom accounts, give each connection a distinct name and select the one you want in each block. + +### Disconnect + +Manage the saved connection in **Integrations** to disconnect it from Sim. Workflows that use it will need another connection. To revoke the Zoom grant, open **Zoom App Marketplace → My Library**, find Sim, and select **More → Remove**. Revoking the grant can affect other Sim connections using that same Zoom account and app. + +Disconnecting does not delete Zoom meetings, recordings, or existing Sim workflow logs. Contact [help@sim.ai](mailto:help@sim.ai) for support or data-deletion requests. + +[Zoom Search](/search/zoom) uses a separate read-only app and connection. A workflow connection does not authorize Search. {/* MANUAL-CONTENT-END */} Connection guides: [Server-to-server OAuth apps](/integrations/zoom-service-account). diff --git a/apps/docs/content/docs/search/zoom.mdx b/apps/docs/content/docs/search/zoom.mdx index bde4fc30578..579dc86032d 100644 --- a/apps/docs/content/docs/search/zoom.mdx +++ b/apps/docs/content/docs/search/zoom.mdx @@ -10,7 +10,7 @@ When Zoom Search is enabled for your organization, connect your own account to f Zoom Search requires deployment enablement and organization access before setup or retrieval is available. Ask your admin about availability if Zoom is missing. 1. An admin adds **Zoom → Member accounts** in **Settings → Sources**. -2. Each person selects **Integrations → Zoom → ⋯ → Connect**, signs in, and authorizes the read permissions. Zoom account-admin approval may be needed. +2. Each person opens **Integrations → Zoom**, selects **Connect**, and authorizes the read permissions. Your Zoom admin may need to approve the app. 3. Return to Sim, confirm the account appears, and search a known meeting. Read the result and follow its source link. A workflow Zoom connection does not authorize Search. Its dedicated app registration prevents Search reauthorization from replacing or narrowing a workflow app's grant. @@ -43,7 +43,7 @@ The connection uses PKCE and `client_secret_basic`. Sim requests only those two ## Disconnect and troubleshoot -In **Integrations**, open Zoom's **…** menu, select **Disconnect** for the account, and confirm. This stops use in the organization. Workflows using that same connection lose access; other members' connections are unaffected. +Open **Integrations → Zoom**, select **Disconnect**, and confirm. Workflows that use this managed connection lose access too. Workflow blocks that use a separate Zoom OAuth connection keep their access. To remove Zoom authorization too, open **Zoom App Marketplace → My Library**, find the Search app, and select **More → Remove**. Account admins may control removal. Disconnecting does not delete meetings/recordings or existing Sim conversations. [Zoom removal instructions](https://support.zoom.com/hc/en/article?id=zm_kb&sysparm_article=KB0062865). From 5123abb6e493e8dae6aaff49dcd5ca1868f15091 Mon Sep 17 00:00:00 2001 From: Waleed Date: Sat, 7 Nov 2026 12:47:49 -0800 Subject: [PATCH 14/29] fix(otp): reserve a verification attempt atomically before comparing the code (#9774) * fix(otp): reserve a verification attempt atomically before comparing the code Chat and public file OTP verify read the attempt count, compared the guess, and only then incremented, so concurrent submissions all passed the limit check on the same stale count and every one was compared. Reserve the attempt first (Redis Lua script / one conditional Postgres UPDATE) and compare only against the reserved value, so at most MAX_OTP_ATTEMPTS guesses are ever checked against one code. Chat verify now uses safeCompare like the file path. * improvement(otp): centralize compare-and-consume in verifyOTP, tighten tests Move the constant-time compare and consume-on-match into verifyOTP so both OTP callers share it; drop getOTP and the decodeOTPValue export now that nothing reads a code without reserving an attempt; remove the unreachable Lua branch; keep the lockout log free of the email address. Tests: assert the Redis TTL is preserved rather than reset, drop the non-null assertion, and use createMockRequest. * fix(otp): keep sub-second code lifetimes and drop unused deleteOTP export TTL rounds a key with under a second left to 0, so the reservation script rewrote it without an expiry and the code never expired. Read PTTL and rewrite with PX, deleting a code with no remaining lifetime. deleteOTP is now only used inside the module. * test(otp): prove rejected verify requests never spend the code against real storage Replace the mock-asserting authorization unit tests with integration cases: with email auth disabled or the address removed from the allow list, five requests carrying the correct code are refused without a cookie, and the code still works once the chat is restored. --- .../[identifier]/otp/route.integration.ts | 193 ++++++++++++++++++ .../api/chat/[identifier]/otp/route.test.ts | 144 +------------ .../app/api/chat/[identifier]/otp/route.ts | 31 +-- apps/sim/lib/core/security/otp.ts | 158 +++++++------- .../content-sharing.integration.ts | 23 ++- .../public-shares/application/credentials.ts | 21 +- 6 files changed, 288 insertions(+), 282 deletions(-) create mode 100644 apps/sim/app/api/chat/[identifier]/otp/route.integration.ts diff --git a/apps/sim/app/api/chat/[identifier]/otp/route.integration.ts b/apps/sim/app/api/chat/[identifier]/otp/route.integration.ts new file mode 100644 index 00000000000..21534fd6cc1 --- /dev/null +++ b/apps/sim/app/api/chat/[identifier]/otp/route.integration.ts @@ -0,0 +1,193 @@ +import { db } from '@sim/db' +import { chat, user, verification, workflow, workspace } from '@sim/db/schema' +import { readTestRedisUrl } from '@sim/db/testing/test-infrastructure' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' +import { createRouteContext } from '@sim/testing/helpers/http' +import { createMockRequest } from '@sim/testing/mocks/request.mock' +import { generateId } from '@sim/utils/id' +import { eq, like } from 'drizzle-orm' +import Redis from 'ioredis' +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' + +const { storage, redisClient } = vi.hoisted(() => ({ + storage: { backend: 'database' as 'database' | 'redis' }, + redisClient: { current: undefined as Redis | undefined }, +})) +vi.mock('@/lib/core/storage', () => ({ getStorageMethod: () => storage.backend })) +vi.mock('@/lib/core/config/redis', async (importOriginal) => ({ + ...(await importOriginal()), + getRedisClient: () => redisClient.current, +})) + +import { MAX_OTP_ATTEMPTS, storeOTP } from '@/lib/core/security/otp' +import { PUT } from '@/app/api/chat/[identifier]/otp/route' + +const redisUrl = readTestRedisUrl() +const ownerId = generateId() +const workspaceId = generateId() +const workflowId = generateId() +const chatId = generateId() +const identifier = `otp-${chatId}` +const email = `${generateId()}@otp.invalid` +const CODE = '246810' +const NO_CODE = 'No verification code found, request a new one' +const INVALID = 'Invalid verification code' +const LOCKED = 'Too many failed attempts. Please request a new code.' + +async function verify(otp: string) { + const request = createMockRequest({ + method: 'PUT', + url: `/api/chat/${identifier}/otp`, + body: { email, otp }, + }) + const response = await PUT(request, createRouteContext({ identifier })) + const body = (await response.json()) as { error?: string; message?: string } + return { + status: response.status, + error: body.error ?? body.message, + cookie: response.headers.get('set-cookie'), + } +} + +/** Distinct wrong six-digit guesses that never equal {@link CODE}. */ +function wrongGuesses(count: number) { + return Array.from({ length: count }, (_, index) => String(100000 + index)) +} + +beforeAll(async () => { + await db.insert(user).values({ + id: ownerId, + name: 'OTP fixture', + email: `${ownerId}@otp.invalid`, + emailVerified: true, + createdAt: new Date(), + updatedAt: new Date(), + }) + await insertWorkspaceFixture(db, { + id: workspaceId, + name: 'OTP fixture', + ownerId, + billedAccountUserId: ownerId, + }) + await db.insert(workflow).values({ + id: workflowId, + userId: ownerId, + workspaceId, + name: 'OTP fixture', + lastSynced: new Date(), + createdAt: new Date(), + updatedAt: new Date(), + }) + await db.insert(chat).values({ + id: chatId, + workflowId, + userId: ownerId, + identifier, + title: 'OTP fixture', + authType: 'email', + allowedEmails: [email], + }) +}) + +afterAll(async () => { + await db.delete(verification).where(like(verification.identifier, `chat-otp:${chatId}:%`)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) + await db.delete(user).where(eq(user.id, ownerId)) + await redisClient.current?.quit() +}) + +describe.each(['database', 'redis'] as const)('%s chat OTP verification', (backend) => { + describe.runIf(backend === 'database' || Boolean(redisUrl))('real storage', () => { + beforeAll(async () => { + if (backend === 'redis' && redisUrl && !redisClient.current) { + redisClient.current = new Redis(redisUrl, { lazyConnect: true, maxRetriesPerRequest: 0 }) + await redisClient.current.connect() + } + }) + beforeEach(async () => { + storage.backend = backend + await storeOTP('chat', chatId, email, CODE) + }) + afterEach(async () => { + await db + .update(chat) + .set({ authType: 'email', allowedEmails: [email] }) + .where(eq(chat.id, chatId)) + }) + + it.each([ + { + rejection: 'the chat no longer uses email auth', + change: { authType: 'public' }, + response: { status: 400, error: 'This chat does not use email authentication' }, + }, + { + rejection: 'the email is no longer allowed', + change: { allowedEmails: ['someone-else@otp.invalid'] }, + response: { status: 403, error: 'Email not authorized' }, + }, + ])('rejects without spending the code when $rejection', async ({ change, response }) => { + await db.update(chat).set(change).where(eq(chat.id, chatId)) + for (let attempt = 0; attempt < MAX_OTP_ATTEMPTS; attempt++) { + expect(await verify(CODE)).toMatchObject({ ...response, cookie: null }) + } + await db + .update(chat) + .set({ authType: 'email', allowedEmails: [email] }) + .where(eq(chat.id, chatId)) + expect(await verify(CODE)).toMatchObject({ status: 200 }) + }) + + it('compares at most the attempt limit of guesses however many arrive concurrently', async () => { + const results = await Promise.all(wrongGuesses(30).map(verify)) + + const compared = results.filter((result) => result.error !== NO_CODE) + expect(compared).toHaveLength(MAX_OTP_ATTEMPTS) + expect(compared.filter((result) => result.error === INVALID)).toHaveLength( + MAX_OTP_ATTEMPTS - 1 + ) + expect(compared.filter((result) => result.error === LOCKED)).toHaveLength(1) + expect(await verify(CODE)).toMatchObject({ status: 400, error: NO_CODE, cookie: null }) + }) + + it('accepts the correct code on the last remaining attempt and mints the auth cookie', async () => { + for (const guess of wrongGuesses(MAX_OTP_ATTEMPTS - 1)) { + expect(await verify(guess)).toMatchObject({ status: 400, error: INVALID }) + } + const accepted = await verify(CODE) + expect(accepted.status).toBe(200) + expect(accepted.cookie).toContain(`chat_auth_${chatId}=`) + expect(await verify(CODE)).toMatchObject({ status: 400, error: NO_CODE }) + }) + + it('keeps the code expiry when a failed attempt is recorded', async () => { + if (backend === 'redis') { + const key = `otp:${email}:${chatId}` + await redisClient.current?.pexpire(key, 900) + expect(await verify('999999')).toMatchObject({ status: 400, error: INVALID }) + const ttl = await redisClient.current?.pttl(key) + expect(ttl).toBeGreaterThan(0) + expect(ttl).toBeLessThanOrEqual(900) + return + } + const readExpiry = async () => + ( + await db + .select({ expiresAt: verification.expiresAt }) + .from(verification) + .where(eq(verification.identifier, `chat-otp:${chatId}:${email}`)) + )[0]?.expiresAt.getTime() + const before = await readExpiry() + expect(await verify('999999')).toMatchObject({ status: 400, error: INVALID }) + expect(await readExpiry()).toBe(before) + }) + + it.runIf(backend === 'database')('rejects an expired code', async () => { + await db + .update(verification) + .set({ expiresAt: new Date(Date.now() - 1000) }) + .where(eq(verification.identifier, `chat-otp:${chatId}:${email}`)) + expect(await verify(CODE)).toMatchObject({ status: 400, error: NO_CODE, cookie: null }) + }) + }) +}) diff --git a/apps/sim/app/api/chat/[identifier]/otp/route.test.ts b/apps/sim/app/api/chat/[identifier]/otp/route.test.ts index 7034b748776..f4a48f6e6cf 100644 --- a/apps/sim/app/api/chat/[identifier]/otp/route.test.ts +++ b/apps/sim/app/api/chat/[identifier]/otp/route.test.ts @@ -24,10 +24,6 @@ import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' const { mockRedisSet, - mockRedisGet, - mockRedisDel, - mockRedisTtl, - mockRedisEval, mockRedisClient, mockSetChatAuthCookie, mockIsEmailAllowed, @@ -36,16 +32,8 @@ const { mockAfterResponse, } = vi.hoisted(() => { const mockRedisSet = vi.fn() - const mockRedisGet = vi.fn() - const mockRedisDel = vi.fn() - const mockRedisTtl = vi.fn() - const mockRedisEval = vi.fn() const mockRedisClient = { set: mockRedisSet, - get: mockRedisGet, - del: mockRedisDel, - ttl: mockRedisTtl, - eval: mockRedisEval, } const mockSetChatAuthCookie = vi.fn() const mockIsEmailAllowed = vi.fn((email: string, allowedEmails: string[]) => { @@ -59,10 +47,6 @@ const { return { mockRedisSet, - mockRedisGet, - mockRedisDel, - mockRedisTtl, - mockRedisEval, mockRedisClient, mockSetChatAuthCookie, mockIsEmailAllowed, @@ -139,7 +123,7 @@ vi.mock('zod', () => { } }) -import { PUT, POST as routePost } from './route' +import { POST as routePost } from './route' const mockCheckRateLimitDirect = rateLimiterMockFns.mockCheckRateLimitDirect const { mockSendEmail } = emailMailerMockFns @@ -156,7 +140,6 @@ describe('Chat OTP API Route', () => { const mockEmail = 'test@example.com' const mockChatId = 'chat-123' const mockIdentifier = 'test-chat' - const mockOTP = '123456' /** Queues the chat-deployment row the route reads before touching OTP storage. */ const queueDeployment = (row: Record) => { @@ -183,9 +166,6 @@ describe('Chat OTP API Route', () => { mockGetRedisClient.mockReturnValue(mockRedisClient) mockRedisSet.mockResolvedValue('OK') - mockRedisGet.mockResolvedValue(null) - mockRedisDel.mockResolvedValue(1) - mockRedisTtl.mockResolvedValue(600) mockGetStorageMethod.mockReturnValue('redis') @@ -312,126 +292,4 @@ describe('Chat OTP API Route', () => { ) }) }) - - describe('PUT - Verify OTP (authType re-check)', () => { - beforeEach(() => { - mockGetStorageMethod.mockReturnValue('redis') - mockRedisGet.mockResolvedValue(`${mockOTP}:0`) - }) - - it('rejects verification when the chat has switched away from email auth', async () => { - queueDeployment({ - id: mockChatId, - authType: 'password', - password: 'encrypted-password', - }) - - const request = createMockRequest({ - method: 'PUT', - url: 'http://localhost:3000/api/chat/test/otp', - rawBody: JSON.stringify({ email: mockEmail, otp: mockOTP }), - }) - - await PUT(request, createRouteContext({ identifier: mockIdentifier })) - - expect(mockCreateCodedErrorResponse).toHaveBeenCalledWith( - 'This chat does not use email authentication', - 400 - ) - expect(mockRedisGet).not.toHaveBeenCalled() - expect(mockSetChatAuthCookie).not.toHaveBeenCalled() - }) - - it('rejects verification when the email is no longer allowed', async () => { - mockIsEmailAllowed.mockReturnValueOnce(false) - queueDeployment({ id: mockChatId, authType: 'email' }) - - const request = createMockRequest({ - method: 'PUT', - url: 'http://localhost:3000/api/chat/test/otp', - rawBody: JSON.stringify({ email: mockEmail, otp: mockOTP }), - }) - - await PUT(request, createRouteContext({ identifier: mockIdentifier })) - - expect(mockCreateCodedErrorResponse).toHaveBeenCalledWith('Email not authorized', 403) - expect(mockRedisGet).not.toHaveBeenCalled() - expect(mockSetChatAuthCookie).not.toHaveBeenCalled() - }) - }) - - describe('PUT - Verify OTP (Database path)', () => { - beforeEach(() => { - mockGetStorageMethod.mockReturnValue('database') - mockGetRedisClient.mockReturnValue(null) - }) - - it('should reject expired OTP from database', async () => { - queueDeployment({ id: mockChatId, authType: 'email' }) - queueTableRows(schemaMock.verification, []) - - const request = createMockRequest({ - method: 'PUT', - url: 'http://localhost:3000/api/chat/test/otp', - rawBody: JSON.stringify({ email: mockEmail, otp: mockOTP }), - }) - - await PUT(request, createRouteContext({ identifier: mockIdentifier })) - - expect(mockCreateCodedErrorResponse).toHaveBeenCalledWith( - 'No verification code found, request a new one', - 400 - ) - }) - }) - - describe('Brute-force protection', () => { - beforeEach(() => { - mockGetStorageMethod.mockReturnValue('redis') - }) - - it('should atomically increment attempts on wrong OTP', async () => { - mockRedisGet.mockResolvedValue('654321:0') - mockRedisEval.mockResolvedValue('654321:1') - - queueDeployment({ id: mockChatId, authType: 'email' }) - - const request = createMockRequest({ - method: 'PUT', - url: 'http://localhost:3000/api/chat/test/otp', - rawBody: JSON.stringify({ email: mockEmail, otp: 'wrong1' }), - }) - - await PUT(request, createRouteContext({ identifier: mockIdentifier })) - - expect(mockRedisEval).toHaveBeenCalledWith( - expect.any(String), - 1, - `otp:${mockEmail}:${mockChatId}`, - 5 - ) - expect(mockCreateCodedErrorResponse).toHaveBeenCalledWith('Invalid verification code', 400) - }) - - it('should invalidate OTP and return 429 after max failed attempts', async () => { - mockRedisGet.mockResolvedValue('654321:4') - mockRedisEval.mockResolvedValue('LOCKED') - - queueDeployment({ id: mockChatId, authType: 'email' }) - - const request = createMockRequest({ - method: 'PUT', - url: 'http://localhost:3000/api/chat/test/otp', - rawBody: JSON.stringify({ email: mockEmail, otp: 'wrong5' }), - }) - - await PUT(request, createRouteContext({ identifier: mockIdentifier })) - - expect(mockRedisEval).toHaveBeenCalled() - expect(mockCreateCodedErrorResponse).toHaveBeenCalledWith( - 'Too many failed attempts. Please request a new code.', - 429 - ) - }) - }) }) diff --git a/apps/sim/app/api/chat/[identifier]/otp/route.ts b/apps/sim/app/api/chat/[identifier]/otp/route.ts index 6651f285702..74d6144677f 100644 --- a/apps/sim/app/api/chat/[identifier]/otp/route.ts +++ b/apps/sim/app/api/chat/[identifier]/otp/route.ts @@ -10,16 +10,12 @@ import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' import { RateLimiter } from '@/lib/core/rate-limiter' import { isEmailAllowed } from '@/lib/core/security/deployment' import { - decodeOTPValue, - deleteOTP, generateOTP, - getOTP, - incrementOTPAttempts, - MAX_OTP_ATTEMPTS, OTP_EMAIL_RATE_LIMIT, OTP_IP_RATE_LIMIT, OTP_RESOURCE_RATE_LIMIT, storeOTP, + verifyOTP, } from '@/lib/core/security/otp' import { afterResponse } from '@/lib/core/utils/after-response' import { generateRequestId, getClientIp } from '@/lib/core/utils/request' @@ -196,33 +192,18 @@ export const PUT = withRouteHandler( return createCodedErrorResponse('Email not authorized', 403) } - const storedValue = await getOTP('chat', deployment.id, email) - if (!storedValue) { + const verification = await verifyOTP('chat', deployment.id, email, otp) + if (verification === 'missing') { return createCodedErrorResponse('No verification code found, request a new one', 400) } - - const { otp: storedOTP, attempts } = decodeOTPValue(storedValue) - - if (attempts >= MAX_OTP_ATTEMPTS) { - await deleteOTP('chat', deployment.id, email) - logger.warn(`OTP already locked out for ${email}`) + if (verification === 'locked') { + logger.warn('OTP invalidated after max failed attempts', { chatId: deployment.id }) return createCodedErrorResponse('Too many failed attempts. Please request a new code.', 429) } - - if (storedOTP !== otp) { - const result = await incrementOTPAttempts('chat', deployment.id, email, storedValue) - if (result === 'locked') { - logger.warn(`OTP invalidated after max failed attempts for ${email}`) - return createCodedErrorResponse( - 'Too many failed attempts. Please request a new code.', - 429 - ) - } + if (verification === 'invalid') { return createCodedErrorResponse('Invalid verification code', 400) } - await deleteOTP('chat', deployment.id, email) - const response = createSuccessResponse({ id: deployment.id, title: deployment.title, diff --git a/apps/sim/lib/core/security/otp.ts b/apps/sim/lib/core/security/otp.ts index 47435216274..3041622c660 100644 --- a/apps/sim/lib/core/security/otp.ts +++ b/apps/sim/lib/core/security/otp.ts @@ -1,8 +1,9 @@ import { randomInt } from 'crypto' import { db } from '@sim/db' import { verification } from '@sim/db/schema' +import { safeCompare } from '@sim/security/compare' import { generateId } from '@sim/utils/id' -import { and, eq, gt } from 'drizzle-orm' +import { and, eq, gt, lt, sql } from 'drizzle-orm' import { getRedisClient } from '@/lib/core/config/redis' import type { TokenBucketConfig } from '@/lib/core/rate-limiter' import { getStorageMethod } from '@/lib/core/storage' @@ -70,7 +71,7 @@ function encodeOTPValue(otp: string, attempts: number): string { return `${otp}:${attempts}` } -export function decodeOTPValue(value: string): { otp: string; attempts: number } { +function decodeOTPValue(value: string): { otp: string; attempts: number } { const lastColon = value.lastIndexOf(':') if (lastColon === -1) return { otp: value, attempts: 0 } const attempts = Number.parseInt(value.slice(lastColon + 1), 10) @@ -115,41 +116,13 @@ export async function storeOTP( }) } -export async function getOTP( - kind: DeploymentKind, - deploymentId: string, - email: string -): Promise { - const keys = OTP_KEYS[kind] - const storageMethod = getStorageMethod() - - if (storageMethod === 'redis') { - const redis = getRedisClient() - if (!redis) throw new Error('Redis configured but client unavailable') - return redis.get(keys.redisKey(email, deploymentId)) - } - - const now = new Date() - const [record] = await db - .select({ value: verification.value }) - .from(verification) - .where( - and( - eq(verification.identifier, keys.dbIdentifier(email, deploymentId)), - gt(verification.expiresAt, now) - ) - ) - .limit(1) - - return record?.value ?? null -} - /** - * Lua script for atomic OTP attempt increment in Redis. - * Returns `'LOCKED'` if max attempts reached (key deleted), new encoded value - * otherwise, nil if key missing. + * Lua script that reserves one verification attempt in Redis. Returns the + * code with the reserved attempt number, or nil when no code exists. Rewrites + * keep the remaining lifetime to the millisecond; a code without one is + * treated as expired so it can never become permanent. */ -const ATOMIC_INCREMENT_SCRIPT = ` +const RESERVE_ATTEMPT_SCRIPT = ` local val = redis.call('GET', KEYS[1]) if not val then return nil end local colon = val:find(':([^:]*)$') @@ -162,31 +135,31 @@ else attempts = 0 end attempts = attempts + 1 +local newVal = otp .. ':' .. attempts if attempts >= tonumber(ARGV[1]) then redis.call('DEL', KEYS[1]) - return 'LOCKED' + return newVal end -local newVal = otp .. ':' .. attempts -local ttl = redis.call('TTL', KEYS[1]) -if ttl > 0 then - redis.call('SET', KEYS[1], newVal, 'EX', ttl) -else - redis.call('SET', KEYS[1], newVal) +local ttl = redis.call('PTTL', KEYS[1]) +if ttl <= 0 then + redis.call('DEL', KEYS[1]) + return nil end +redis.call('SET', KEYS[1], newVal, 'PX', ttl) return newVal ` /** - * Atomically increments an OTP's failed-attempt counter. Returns `'locked'` - * if the max-attempts threshold was reached (and the OTP was deleted), or - * `'incremented'` otherwise. The DB path uses optimistic locking with retry. + * Atomically spends one of the code's {@link MAX_OTP_ATTEMPTS} attempts and + * returns the stored code with its reserved attempt count, or `null` when no + * live code is left. The reservation that spends the last attempt also + * deletes the code. */ -export async function incrementOTPAttempts( +async function reserveOTPAttempt( kind: DeploymentKind, deploymentId: string, - email: string, - currentValue: string -): Promise<'locked' | 'incremented'> { + email: string +): Promise<{ otp: string; attempts: number } | null> { const keys = OTP_KEYS[kind] const storageMethod = getStorageMethod() @@ -194,53 +167,62 @@ export async function incrementOTPAttempts( const redis = getRedisClient() if (!redis) throw new Error('Redis configured but client unavailable') const key = keys.redisKey(email, deploymentId) - const result = await redis.eval(ATOMIC_INCREMENT_SCRIPT, 1, key, MAX_OTP_ATTEMPTS) - if (result === null || result === 'LOCKED') return 'locked' - return 'incremented' + const result = await redis.eval(RESERVE_ATTEMPT_SCRIPT, 1, key, MAX_OTP_ATTEMPTS) + return typeof result === 'string' ? decodeOTPValue(result) : null } const identifier = keys.dbIdentifier(email, deploymentId) - const MAX_RETRIES = 3 - let value = currentValue - - for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { - const { otp, attempts } = decodeOTPValue(value) - const newAttempts = attempts + 1 - - if (newAttempts >= MAX_OTP_ATTEMPTS) { - await db.delete(verification).where(eq(verification.identifier, identifier)) - return 'locked' - } - - const newValue = encodeOTPValue(otp, newAttempts) - const updated = await db - .update(verification) - .set({ value: newValue, updatedAt: new Date() }) - .where(and(eq(verification.identifier, identifier), eq(verification.value, value))) - .returning({ id: verification.id }) - - if (updated.length > 0) return 'incremented' - - const fresh = await getOTP(kind, deploymentId, email) - if (!fresh) return 'locked' - value = fresh + // Pattern match, not split + cast: other `verification` rows must never fail a cast. + const attemptsSql = sql`substring(${verification.value} from ':([0-9]+)$')::int` + const [reserved] = await db + .update(verification) + .set({ + value: sql`substring(${verification.value} from '^(.*):[0-9]+$') || ':' || (${attemptsSql} + 1)`, + updatedAt: new Date(), + }) + .where( + and( + eq(verification.identifier, identifier), + gt(verification.expiresAt, new Date()), + lt(attemptsSql, MAX_OTP_ATTEMPTS) + ) + ) + .returning({ value: verification.value }) + + if (!reserved) return null + const decoded = decodeOTPValue(reserved.value) + if (decoded.attempts >= MAX_OTP_ATTEMPTS) { + await db + .delete(verification) + .where(and(eq(verification.identifier, identifier), eq(verification.value, reserved.value))) } - - /** - * Retry exhaustion under heavy DB-path contention: this request did not - * succeed in writing its own +1, so the stored count may not reflect it. - * Fail closed — invalidate the OTP rather than return `'incremented'` with - * a possibly-undercounted attempt total. - */ - await db.delete(verification).where(eq(verification.identifier, identifier)) - return 'locked' + return decoded } -export async function deleteOTP( +export type OTPVerification = 'valid' | 'invalid' | 'locked' | 'missing' + +/** + * Checks a submitted code and consumes it on a match. An attempt is reserved + * before the comparison, so at most {@link MAX_OTP_ATTEMPTS} guesses are ever + * compared against one code however many requests run concurrently. + * `'locked'` means this guess spent the last attempt and the code is gone. + */ +export async function verifyOTP( kind: DeploymentKind, deploymentId: string, - email: string -): Promise { + email: string, + code: string +): Promise { + const reserved = await reserveOTPAttempt(kind, deploymentId, email) + if (!reserved) return 'missing' + if (safeCompare(reserved.otp, code)) { + await deleteOTP(kind, deploymentId, email) + return 'valid' + } + return reserved.attempts >= MAX_OTP_ATTEMPTS ? 'locked' : 'invalid' +} + +async function deleteOTP(kind: DeploymentKind, deploymentId: string, email: string): Promise { const keys = OTP_KEYS[kind] const storageMethod = getStorageMethod() diff --git a/apps/sim/lib/projects/files/__integration__/content-sharing.integration.ts b/apps/sim/lib/projects/files/__integration__/content-sharing.integration.ts index 841573b0f67..3201a0c2af7 100644 --- a/apps/sim/lib/projects/files/__integration__/content-sharing.integration.ts +++ b/apps/sim/lib/projects/files/__integration__/content-sharing.integration.ts @@ -16,6 +16,7 @@ import { publicShare, subscription, user, + verification, workspace, workspaceFileSecretProvenance, workspaceFiles, @@ -574,7 +575,14 @@ describe('Public file shares against PostgreSQL and private storage', () => { await ( await requestPublicFileShareOtp({ token: share.token, email: 'outsider@shared.invalid' }) ).deliver() - expect(await otpStore.getOTP('file', share.id, 'outsider@shared.invalid')).toBeNull() + const storedCode = async (address: string) => { + const [row] = await db + .select({ value: verification.value }) + .from(verification) + .where(eq(verification.identifier, `file-otp:${share.id}:${address}`)) + return row?.value.split(':')[0] + } + expect(await storedCode('outsider@shared.invalid')).toBeUndefined() const failure = { authorized: false, status: 400, error: 'Invalid verification code' } expect( await verifyPublicFileShareOtp({ @@ -587,9 +595,8 @@ describe('Public file shares against PostgreSQL and private storage', () => { failure ) await (await requestPublicFileShareOtp({ token: share.token, email })).deliver() - const stored = await otpStore.getOTP('file', share.id, email) - if (!stored) throw new Error('Allowed email received no code') - const { otp } = otpStore.decodeOTPValue(stored) + const otp = await storedCode(email) + if (!otp) throw new Error('Allowed email received no code') expect( await verifyPublicFileShareOtp({ token: share.token, email, otp: '000000' }) ).toMatchObject({ authorized: false, status: 400 }) @@ -606,9 +613,9 @@ describe('Public file shares against PostgreSQL and private storage', () => { status: 400, }) await otpStore.storeOTP('file', share.id, email, '123456') - const get = otpStore.getOTP - const spy = vi.spyOn(otpStore, 'getOTP').mockImplementation(async (...args) => { - const result = await get(...args) + const verifyCode = otpStore.verifyOTP + const spy = vi.spyOn(otpStore, 'verifyOTP').mockImplementation(async (...args) => { + const result = await verifyCode(...args) await db .update(publicShare) .set({ allowedEmails: ['replacement@shared.invalid'] }) @@ -619,7 +626,7 @@ describe('Public file shares against PostgreSQL and private storage', () => { verifyPublicFileShareOtp({ token: share.token, email, otp: '123456' }) ).rejects.toMatchObject({ code: 'not_found' }) spy.mockRestore() - await otpStore.deleteOTP('file', share.id, email) + expect(await storedCode(email)).toBeUndefined() } ) diff --git a/apps/sim/lib/public-shares/application/credentials.ts b/apps/sim/lib/public-shares/application/credentials.ts index e8036c4e50e..27737f346c2 100644 --- a/apps/sim/lib/public-shares/application/credentials.ts +++ b/apps/sim/lib/public-shares/application/credentials.ts @@ -1,19 +1,14 @@ import { createLogger } from '@sim/logger' -import { safeCompare } from '@sim/security/compare' import { normalizeEmail } from '@sim/utils/string' import { getOtpSubject, renderOTPEmail } from '@/components/emails' import { RateLimiter, type TokenBucketConfig } from '@/lib/core/rate-limiter' import { createDeploymentAuthCookie, isEmailAllowed } from '@/lib/core/security/deployment' import { - decodeOTPValue, - deleteOTP, generateOTP, - getOTP, - incrementOTPAttempts, - MAX_OTP_ATTEMPTS, OTP_EMAIL_RATE_LIMIT, OTP_RESOURCE_RATE_LIMIT, storeOTP, + verifyOTP, } from '@/lib/core/security/otp' import { sendEmail } from '@/lib/messaging/email/mailer' import { @@ -114,19 +109,9 @@ export async function verifyPublicFileShareOtp({ isEmailAllowed(normalizedEmail, share.allowedEmails) ) if (!allowed) return OTP_FAILURE - const value = await getOTP('file', challenge.shareId, normalizedEmail) + const verification = await verifyOTP('file', challenge.shareId, normalizedEmail, otp) await challenge.withCurrentPolicy(() => undefined) - if (!value) return OTP_FAILURE - const stored = decodeOTPValue(value) - if (stored.attempts >= MAX_OTP_ATTEMPTS) { - await deleteOTP('file', challenge.shareId, normalizedEmail) - return OTP_FAILURE - } - if (!safeCompare(stored.otp, otp)) { - await incrementOTPAttempts('file', challenge.shareId, normalizedEmail, value) - return OTP_FAILURE - } - await deleteOTP('file', challenge.shareId, normalizedEmail) + if (verification !== 'valid') return OTP_FAILURE const resource = await challenge.withCurrentPolicy(({ share }) => share) const cookie = await createDeploymentAuthCookie({ cookiePrefix: 'file', From 07a8d0c5bb8b02180dae14e8a095bfc7adf9a43f Mon Sep 17 00:00:00 2001 From: Waleed Date: Sat, 7 Nov 2026 12:52:33 -0800 Subject: [PATCH 15/29] feat(landing): redirect /security and /trust to the Trust Center (#9780) --- apps/sim/next.config.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/sim/next.config.ts b/apps/sim/next.config.ts index cc068c00f56..b72ac6efc06 100644 --- a/apps/sim/next.config.ts +++ b/apps/sim/next.config.ts @@ -493,6 +493,16 @@ const nextConfig: NextConfig = { source: '/team', destination: 'https://cal.com/team/sim/demo', permanent: false, + }, + { + source: '/security', + destination: 'https://trust.sim.ai/', + permanent: false, + }, + { + source: '/trust', + destination: 'https://trust.sim.ai/', + permanent: false, } ) From e820b586fbae7ed79be864328fd49b5594d99f82 Mon Sep 17 00:00:00 2001 From: Mukhamediyar <86684638+myxamediyar@users.noreply.github.com> Date: Sat, 7 Nov 2026 13:03:54 -0800 Subject: [PATCH 16/29] feat(video): accept a start frame image for fal.ai video models (#9761) * feat(video): accept a start frame image for fal.ai video models Fal models with an image-to-video sibling endpoint now take an optional start frame. When one is attached, requests go to the image-to-video endpoint with the image as a data URI in the model's image field, and the block's settings dropdowns follow that endpoint's options. Co-Authored-By: Claude Opus 5.5 * fix(video): keep fal settings enabled without a start frame - Drop the optional start frame fields from `dependsOn`: an array there disables the field until every entry is filled, which greyed out the settings for text-only runs. Options and conditions already re-read all block values. - Pass the run's abort signal through to the start frame download. - The fal tool now offers `visualReference` to agents for models with an image-to-video endpoint; update the provider selection test. Co-Authored-By: Claude Opus 5.5 * fix(video): send the model default for fal settings a frame change invalidates Attaching or removing a start frame switches a fal model between its text-to-video and image-to-video endpoints, and the settings picked for one can be invalid on the other (Seedance 2.5 takes 16:9 from a prompt but only auto from a frame). The V3 block now drops any stored setting the selected endpoint does not accept, so the run uses the model default instead of failing validation. Co-Authored-By: Claude Opus 5.5 * fix(video): drop only settings a start frame change made stale Runway models also switch endpoints when a start frame is attached, and their stored settings can be valid on one and not the other (Kling 4 takes 1920:1080 from a prompt but only auto ratios from a frame). Both fal and direct providers now drop a setting only when the selected endpoint rejects it and the model's other endpoint accepts it. Values neither endpoint lists still reach the request builder, which maps legacy Runway ratios such as 16:9 and rejects anything else. Co-Authored-By: Claude Opus 5.5 * fix(video): check fal start frame provenance before sending it to the model A fal start frame now passes the same opaque model-input provenance check as a Runway reference before its bytes reach the model. * fix(video): pick fal settings from the active start frame input The Reference Image is a basic upload / advanced reference pair, and the serializer sends only the active member. The fal settings fields counted a file left in the inactive member too, so switching to an empty advanced input kept showing the start frame endpoint's settings while the run called the prompt endpoint. Resolve the active member with `resolveActiveCanonicalValue`, and pass `__canonicalModes` to dropdown option functions as the editor already does for conditions, so a field's visibility and its options agree. Co-Authored-By: Claude Opus 5.5 * fix(video): reject a reference file that is not an image before submitting An agent can pass any file as the start frame or Runway reference, and nothing checked its type before the paid request. Validation now returns a 400 for a non-image `visualReference` before the file is downloaded. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: Waleed Latif --- .../components/dropdown/dropdown.tsx | 12 +- .../sim/blocks/blocks/video_generator.test.ts | 41 + apps/sim/blocks/blocks/video_generator.ts | 200 +++- apps/sim/lib/internal/video/client.test.ts | 82 +- apps/sim/lib/internal/video/client.ts | 43 +- apps/sim/lib/internal/video/fal-request.ts | 29 +- .../sim/lib/internal/video/operations.test.ts | 43 +- apps/sim/lib/internal/video/operations.ts | 2 +- apps/sim/lib/media/fal-video-catalog.ts | 31 +- apps/sim/lib/media/fal-video-models.json | 857 ++++++++++++++++-- apps/sim/lib/tools/falai-pricing.test.ts | 2 + apps/sim/lib/tools/falai-pricing.ts | 4 +- apps/sim/providers/utils.test.ts | 2 +- apps/sim/scripts/validate-fal-video-models.ts | 51 +- apps/sim/serializer/field-analysis.test.ts | 81 ++ apps/sim/tools/generated/tool-metadata.ts | 2 +- apps/sim/tools/video/falai.ts | 11 +- 17 files changed, 1304 insertions(+), 189 deletions(-) create mode 100644 apps/sim/blocks/blocks/video_generator.test.ts diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx index 5496e2e20c4..96dfa8108af 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx @@ -164,15 +164,19 @@ export const Dropdown = memo(function Dropdown({ }, [multiSelect, value]) // Derived option lists read the block's own values (a model's valid reasoning efforts); - // `dependsOn` already re-renders this control when one of those siblings changes. + // `dependsOn` already re-renders this control when one of those siblings changes. They also get + // `__canonicalModes`, as conditions do, so both read the same active member of a canonical pair. const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId) const blockValues = useSubBlockStore((state) => activeWorkflowId ? state.workflowValues[activeWorkflowId]?.[blockId] : undefined ) const evaluatedOptions = useMemo(() => { - if (typeof options === 'function') return options({ values: blockValues ?? {} }) - return options ?? EMPTY_OPTIONS - }, [options, blockValues]) + if (typeof options !== 'function') return options ?? EMPTY_OPTIONS + const values = blockValues ?? {} + return options({ + values: canonicalModes ? { ...values, __canonicalModes: canonicalModes } : values, + }) + }, [options, blockValues, canonicalModes]) const reactiveOptions = useMemo( () => diff --git a/apps/sim/blocks/blocks/video_generator.test.ts b/apps/sim/blocks/blocks/video_generator.test.ts new file mode 100644 index 00000000000..5b2fd18c33c --- /dev/null +++ b/apps/sim/blocks/blocks/video_generator.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest' +import { evaluateSubBlockCondition } from '@/lib/workflows/subblocks/visibility' +import { VideoGeneratorV3Block } from '@/blocks/blocks/video_generator' + +const frame = { id: 'f', name: 'frame.png', url: '', size: 3, type: 'image/png', key: 'k' } + +function aspectRatioField() { + const field = VideoGeneratorV3Block.subBlocks.find((subBlock) => subBlock.id === 'falAspectRatio') + if (!field) throw new Error('Video Generator must expose a fal aspect ratio field') + return field +} + +function optionIds(values: Record) { + const { options } = aspectRatioField() + if (typeof options !== 'function') throw new Error('Fal aspect ratio options must be derived') + return options({ values }).map((option) => (typeof option === 'string' ? option : option.id)) +} + +describe('Video Generator fal start frame', () => { + // Happy Horse takes an aspect ratio from a prompt but not from a start frame. + const base = { provider: 'falai', model: 'alibaba/happy-horse/text-to-video' } + + it('reads the prompt endpoint when the stored upload is in the inactive mode', () => { + const values = { + ...base, + visualReferenceUpload: frame, + visualReferenceInput: '', + __canonicalModes: { visualReference: 'advanced' }, + } + + expect(evaluateSubBlockCondition(aspectRatioField().condition, values)).toBe(true) + expect(optionIds(values)).toContain('16:9') + }) + + it('reads the start frame endpoint when the upload is the active mode', () => { + const values = { ...base, visualReferenceUpload: frame } + + expect(evaluateSubBlockCondition(aspectRatioField().condition, values)).toBe(false) + expect(optionIds(values)).not.toContain('16:9') + }) +}) diff --git a/apps/sim/blocks/blocks/video_generator.ts b/apps/sim/blocks/blocks/video_generator.ts index a280cb9a29f..f03a36463d9 100644 --- a/apps/sim/blocks/blocks/video_generator.ts +++ b/apps/sim/blocks/blocks/video_generator.ts @@ -1,10 +1,21 @@ +import { isRecordLike } from '@sim/utils/object' import { VideoIcon } from '@/components/icons' import { directVideoDefaults, directVideoModels, getDirectVideoModel, } from '@/lib/media/direct-video-catalog' -import { falVideoModels, getFalVideoModel } from '@/lib/media/fal-video-catalog' +import { + type FalVideoSettings, + falVideoModels, + getFalVideoModel, + getFalVideoVariant, +} from '@/lib/media/fal-video-catalog' +import { + type CanonicalGroup, + type CanonicalModeOverrides, + resolveActiveCanonicalValue, +} from '@/lib/workflows/subblocks/visibility' import { AuthMode, type BlockConfig, IntegrationType, type SubBlockConfig } from '@/blocks/types' import { normalizeFileInput, parseOptionalBooleanInput } from '@/blocks/utils' @@ -42,40 +53,69 @@ const FALAI_AUDIO_DEFAULT_OFF_MODELS = ['kling-o3-pro', 'kling-o3-4k'] const FAL_MODEL_DEFAULT = { id: 'default', label: 'Model default' } as const const endpointModels = falVideoModels.filter((model) => !model.legacyId) -function tuningField( - id: string, - title: string, - key: 'durations' | 'aspectRatios' | 'resolutions' | 'audioField' | 'promptOptimizer' -): SubBlockConfig { +// Never list these optional fields in `dependsOn`: an array there disables the field until every +// entry has a value. Options and conditions already re-read all block values on any change. +const START_FRAME_FIELDS = ['visualReferenceInput', 'visualReferenceUpload'] +const HIDDEN = { field: 'provider', value: [] } + +const START_FRAME_GROUP: CanonicalGroup = { + canonicalId: 'visualReference', + basicId: 'visualReferenceUpload', + advancedIds: ['visualReferenceInput'], +} + +/** + * Whether the run will send a start frame: the serialized `visualReference`, or else only the + * upload/reference member in the active mode, since the serializer drops the inactive one. + */ +function hasStartFrame(values?: Record) { + if (!values) return false + const overrides = isRecordLike(values.__canonicalModes) + ? (values.__canonicalModes as CanonicalModeOverrides) + : undefined + const reference = + values.visualReference ?? resolveActiveCanonicalValue(START_FRAME_GROUP, values, overrides) + return Array.isArray(reference) ? reference.length > 0 : Boolean(reference) +} + +/** Settings of the endpoint a fal run will call: image-to-video once a start frame is attached. */ +function selectedFalSettings(values?: Record) { + const model = getFalVideoModel(values?.model) + return getFalVideoVariant(values?.model, Boolean(model?.image) && hasStartFrame(values)) +} + +type FalSettingKey = 'durations' | 'aspectRatios' | 'resolutions' | 'audioField' | 'promptOptimizer' + +function tuningField(id: string, title: string, key: FalSettingKey): SubBlockConfig { return { id, title, type: 'dropdown', value: () => FAL_MODEL_DEFAULT.id, - condition: { - field: 'provider', - value: 'falai', - and: { - field: 'model', - value: endpointModels.filter((model) => model[key]).map((model) => model.modelId), - }, + condition: (values) => { + const model = getFalVideoModel(values?.model) + return model && !model.legacyId && selectedFalSettings(values)?.[key] + ? { field: 'provider', value: 'falai' } + : HIDDEN }, dependsOn: ['model'], options: (params) => { if (!params) { - const values = endpointModels.flatMap((model) => { - const choices = model[key] - return Array.isArray(choices) - ? choices.map((value) => - key === 'durations' ? String(Number.parseFloat(String(value))) : String(value) - ) - : choices - ? ['true', 'false'] - : [] - }) + const values = endpointModels.flatMap((model) => + [model, model.image].flatMap((settings) => { + const choices = settings?.[key] + return Array.isArray(choices) + ? choices.map((value) => + key === 'durations' ? String(Number.parseFloat(String(value))) : String(value) + ) + : choices + ? ['true', 'false'] + : [] + }) + ) return [FAL_MODEL_DEFAULT, ...[...new Set(values)].map((id) => ({ id, label: id }))] } - const model = getFalVideoModel(params?.values.model) + const model = selectedFalSettings(params?.values) if (!model?.[key]) return [] if (key === 'audioField' || key === 'promptOptimizer') { return [FAL_MODEL_DEFAULT, { id: 'true', label: 'On' }, { id: 'false', label: 'Off' }] @@ -109,6 +149,49 @@ const falVideoFields = [ tuningField('falPromptOptimizer', 'Prompt Optimizer', 'promptOptimizer'), ] +/** Settings fields that models with a legacy ID keep, and the catalog setting each one sets. */ +const LEGACY_FAL_SETTING_KEYS: Record = { + duration: 'durations', + aspectRatio: 'aspectRatios', + resolution: 'resolutions', + generateAudio: 'audioField', + promptOptimizer: 'promptOptimizer', +} + +/** + * Drops a stored setting the endpoint a run calls rejects but the model's other endpoint accepts, + * so the model default applies. Attaching or removing a start frame switches endpoints without + * clearing settings picked for the other one: Seedance 2.5 takes `16:9` from a prompt but only + * `auto` from a start frame. Any other value passes through to the request's own legacy mapping + * and validation. + */ +function dropStaleSetting( + value: unknown, + allowed: readonly (number | string)[] | undefined, + otherAllowed: readonly (number | string)[] | undefined, + numeric = false +) { + const normalize = (choice: unknown) => (numeric ? Number.parseFloat(String(choice)) : choice) + const accepts = (choices?: readonly (number | string)[]) => + choices?.some((choice) => normalize(choice) === normalize(value)) ?? false + return value !== undefined && !accepts(allowed) && accepts(otherAllowed) ? undefined : value +} + +function keepFalSettingsAllowed( + params: Record, + settings: FalVideoSettings | undefined, + other: FalVideoSettings | undefined +) { + return { + ...params, + duration: dropStaleSetting(params.duration, settings?.durations, other?.durations, true), + aspectRatio: dropStaleSetting(params.aspectRatio, settings?.aspectRatios, other?.aspectRatios), + resolution: dropStaleSetting(params.resolution, settings?.resolutions, other?.resolutions), + generateAudio: settings?.audioField ? params.generateAudio : undefined, + promptOptimizer: settings?.promptOptimizer ? params.promptOptimizer : undefined, + } +} + function withFalVideoCatalog(subBlocks: SubBlockConfig[]): SubBlockConfig[] { return subBlocks.flatMap((subBlock) => { const condition = subBlock.condition @@ -141,17 +224,23 @@ function withFalVideoCatalog(subBlocks: SubBlockConfig[]): SubBlockConfig[] { ] } if (subBlock.id === 'apiKey' && !condition.not) return [...falVideoFields, subBlock] + const key = LEGACY_FAL_SETTING_KEYS[subBlock.id] + if (key) + return [ + { + ...subBlock, + condition: (values) => { + const image = getFalVideoModel(values?.model)?.image + return image && !image[key] && hasStartFrame(values) ? HIDDEN : condition + }, + }, + ] return [subBlock] }) } function selectedDirectModel(values?: Record) { - const withImage = [ - values?.visualReference, - values?.visualReferenceInput, - values?.visualReferenceUpload, - ].some((reference) => (Array.isArray(reference) ? reference.length > 0 : Boolean(reference))) - return getDirectVideoModel(values?.provider, values?.model, withImage) + return getDirectVideoModel(values?.provider, values?.model, hasStartFrame(values)) } const directVideoFields = ( @@ -168,7 +257,7 @@ const directVideoFields = ( title, type: key === 'promptOptimizer' ? 'switch' : 'dropdown', value: key === 'promptOptimizer' ? undefined : () => FAL_MODEL_DEFAULT.id, - dependsOn: ['provider', 'model', 'visualReferenceInput', 'visualReferenceUpload'], + dependsOn: ['provider', 'model'], condition: (values) => ({ field: 'provider', value: selectedDirectModel(values)?.settings?.[key] ? [String(values?.provider)] : [], @@ -302,9 +391,18 @@ function withDirectVideoCatalog(subBlocks: SubBlockConfig[]): SubBlockConfig[] { dependsOn: ['provider', 'model'], }, ] - if (field.id === 'visualReferenceUpload' || field.id === 'visualReferenceInput') + if (START_FRAME_FIELDS.includes(field.id)) return [ - { ...field, required: { field: 'model', value: ['gen4_turbo', 'gen-4', 'gen-4-turbo'] } }, + { + ...field, + condition: (values) => + values?.provider === 'runway' || + (values?.provider === 'falai' && getFalVideoModel(values.model)?.image) + ? { field: 'provider', value: ['runway', 'falai'] } + : HIDDEN, + dependsOn: ['provider', 'model'], + required: { field: 'model', value: ['gen4_turbo', 'gen-4', 'gen-4-turbo'] }, + }, ] return [field] }) @@ -1950,19 +2048,37 @@ export const VideoGeneratorV3Block: BlockConfig = { ? 'video_higgsfield' : (VideoGeneratorV2Block.tools.config?.tool(params) ?? 'video_runway'), params: (params) => { - if (params.provider === 'falai') - return VideoGeneratorV2Block.tools.config?.params?.(params) ?? params + if (params.provider === 'falai') { + const falParams = VideoGeneratorV2Block.tools.config?.params?.(params) ?? params + const model = getFalVideoModel(params.model) + if (!model) return { ...falParams, visualReference: undefined } + const withImage = Boolean(model.image && falParams.visualReference) + return { + ...keepFalSettingsAllowed( + falParams, + getFalVideoVariant(params.model, withImage), + getFalVideoVariant(params.model, !withImage) + ), + visualReference: withImage ? falParams.visualReference : undefined, + } + } const visualReference = params.provider === 'runway' ? normalizeFileInput(params.visualReference, { single: true }) : undefined const model = getDirectVideoModel(params.provider, params.model, Boolean(visualReference)) + const other = getDirectVideoModel(params.provider, params.model, !visualReference)?.settings const setting = (key: string, supported: unknown, fallbackKey?: string) => { if (!supported) return undefined const value = params[key] ?? (fallbackKey ? params[fallbackKey] : undefined) return value === FAL_MODEL_DEFAULT.id ? undefined : value } - const duration = setting('duration', model?.settings?.durations) + const duration = dropStaleSetting( + setting('duration', model?.settings?.durations), + model?.settings?.durations, + other?.durations, + true + ) return { provider: params.provider, model: params.model, @@ -1970,8 +2086,16 @@ export const VideoGeneratorV3Block: BlockConfig = { prompt: params.prompt, visualReference, duration: duration ? Number(duration) : undefined, - aspectRatio: setting('aspectRatio', model?.settings?.aspectRatios), - resolution: setting('resolution', model?.settings?.resolutions), + aspectRatio: dropStaleSetting( + setting('aspectRatio', model?.settings?.aspectRatios), + model?.settings?.aspectRatios, + other?.aspectRatios + ), + resolution: dropStaleSetting( + setting('resolution', model?.settings?.resolutions), + model?.settings?.resolutions, + other?.resolutions + ), generateAudio: parseOptionalBooleanInput( setting('directGenerateAudio', model?.settings?.audio, 'generateAudio') ), diff --git a/apps/sim/lib/internal/video/client.test.ts b/apps/sim/lib/internal/video/client.test.ts index 5947d6f329e..0c44897f5d2 100644 --- a/apps/sim/lib/internal/video/client.test.ts +++ b/apps/sim/lib/internal/video/client.test.ts @@ -3,6 +3,10 @@ import { executionLimitsMock, executionLimitsMockFns, } from '@sim/testing/mocks/execution-limits.mock' +import { + fileUtilsServerMock, + fileUtilsServerMockFns, +} from '@sim/testing/mocks/file-utils-server.mock' import { inputValidationMock, inputValidationMockFns, @@ -14,8 +18,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('@/lib/uploads', () => uploadsMock) vi.mock('@/lib/core/execution-limits', () => executionLimitsMock) vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock) +vi.mock('@/lib/uploads/utils/file.server', () => fileUtilsServerMock) -import { generateVideo } from '@/lib/internal/video/client' +import { generateVideo, getVideoInputValidationError } from '@/lib/internal/video/client' import { executeVideoOperation } from '@/lib/internal/video/operations' import { observeServiceCosts } from '@/lib/mothership/billing/service-observer' @@ -472,6 +477,81 @@ describe('Video provider client', () => { expect(fetchMock).not.toHaveBeenCalled() }) + it.each([ + { + model: 'kling-v3-pro', + endpoint: 'fal-ai/kling-video/v3/pro/image-to-video', + imageField: 'start_image_url', + }, + { + model: 'veo-3.1', + endpoint: 'fal-ai/veo3.1/image-to-video', + imageField: 'image_url', + }, + ])('sends a start frame to $endpoint as $imageField', async ({ model, endpoint, imageField }) => { + fileUtilsServerMockFns.mockDownloadFileFromStorage.mockResolvedValueOnce(Buffer.from('png')) + const mockFetch = vi + .fn() + .mockResolvedValueOnce(jsonResponse({ request_id: 'fal-1' })) + .mockResolvedValueOnce(jsonResponse({ status: 'COMPLETED' })) + .mockResolvedValueOnce(jsonResponse({ video: { url: 'data:video/mp4;base64,dmlkZW8=' } })) + vi.stubGlobal('fetch', mockFetch) + const frame = { id: 'f', name: 'frame.png', url: '', size: 3, type: 'image/png', key: 'k' } + + const result = generateVideo( + { provider: 'falai', apiKey: 'key', model, prompt: 'Pan left', visualReference: frame }, + { requestId: 'request-1' } + ) + await Promise.all([result, vi.advanceTimersByTimeAsync(5000)]) + + expect(String(mockFetch.mock.calls[0]?.[0])).toBe(`https://queue.fal.run/${endpoint}`) + expect(JSON.parse(String(mockFetch.mock.calls[0]?.[1]?.body))).toEqual( + expect.objectContaining({ + prompt: 'Pan left', + [imageField]: `data:image/png;base64,${Buffer.from('png').toString('base64')}`, + }) + ) + }) + + it.each([ + { provider: 'falai' as const, model: 'kling-v3-pro' }, + { provider: 'runway' as const, model: 'gen4_turbo' }, + ])( + 'rejects a non-image reference file for $provider before submitting', + ({ provider, model }) => { + const pdf = { + id: 'f', + name: 'brief.pdf', + url: '', + size: 3, + type: 'application/pdf', + key: 'k', + } + expect( + getVideoInputValidationError({ + provider, + apiKey: 'key', + model, + prompt: 'Pan left', + visualReference: pdf, + }) + ).toBe('Reference image must be an image file, not application/pdf') + } + ) + + it('rejects a start frame for a fal model without an image-to-video endpoint', () => { + const frame = { id: 'f', name: 'frame.png', url: '', size: 3, type: 'image/png', key: 'k' } + expect( + getVideoInputValidationError({ + provider: 'falai', + apiKey: 'key', + model: 'minimax/h3-max/styles/vhs', + prompt: 'Pan left', + visualReference: frame, + }) + ).toBe('Fal.ai model minimax/h3-max/styles/vhs does not accept a start frame image') + }) + it('retrieves a nested endpoint result when fal omits the queue URLs', async () => { const app = 'fal-ai/kandinsky5' const model = `${app}/text-to-video` diff --git a/apps/sim/lib/internal/video/client.ts b/apps/sim/lib/internal/video/client.ts index cffb6dfeb0c..1972d909937 100644 --- a/apps/sim/lib/internal/video/client.ts +++ b/apps/sim/lib/internal/video/client.ts @@ -94,6 +94,8 @@ async function waitForProvider(context: VideoGenerationContext): Promise { } export function getVideoInputValidationError(input: VideoGenerationInput): string | undefined { + if (input.visualReference && !input.visualReference.type.toLowerCase().startsWith('image/')) + return `Reference image must be an image file, not ${input.visualReference.type || 'an unknown type'}` if (input.provider === 'falai') { if (input.prompt.length < 3 || input.prompt.length > 2000) return 'Prompt must be between 3 and 2000 characters' @@ -122,13 +124,30 @@ export async function generateVideo( : generateWithDirectProvider(input, context) } +/** Keeps the encoded image under 5 MB, the largest data URI Runway accepts. */ +const MAX_REFERENCE_IMAGE_BYTES = Math.floor(((5 * 1024 * 1024 - 512) * 3) / 4) + +async function readReferenceImageDataUri( + file: UserFile, + { requestId, signal }: VideoGenerationContext, + label: string +): Promise { + assertKnownSizeWithinLimit(file.size, MAX_REFERENCE_IMAGE_BYTES, label) + const buffer = await downloadFileFromStorage(file, requestId, logger, { + maxBytes: MAX_REFERENCE_IMAGE_BYTES, + signal, + }) + assertKnownSizeWithinLimit(buffer.length, MAX_REFERENCE_IMAGE_BYTES, label) + return `data:${file.type};base64,${buffer.toString('base64')}` +} + async function generateWithDirectProvider( input: VideoGenerationInput, context: VideoGenerationContext ): Promise { const prepared = buildDirectVideoRequest(input) const { protocol } = prepared.model - const { signal, requestId } = context + const { signal } = context const headers: Record = { 'Content-Type': 'application/json' } if (protocol === 'gemini') { headers['x-goog-api-key'] = input.apiKey @@ -138,17 +157,11 @@ async function generateWithDirectProvider( if (protocol === 'runway') headers['X-Runway-Version'] = '2024-11-06' if (input.visualReference) { - const maxReferenceBytes = Math.floor(((5 * 1024 * 1024 - 512) * 3) / 4) - assertKnownSizeWithinLimit( - input.visualReference.size, - maxReferenceBytes, + prepared.body.promptImage = await readReferenceImageDataUri( + input.visualReference, + context, 'Runway reference image' ) - const buffer = await downloadFileFromStorage(input.visualReference, requestId, logger, { - maxBytes: maxReferenceBytes, - }) - assertKnownSizeWithinLimit(buffer.length, maxReferenceBytes, 'Runway reference image') - prepared.body.promptImage = `data:${input.visualReference.type};base64,${buffer.toString('base64')}` } signal?.throwIfAborted() const readProviderResponse = async (response: Response): Promise> => { @@ -388,7 +401,15 @@ async function generateWithFalAI( const { requestId, signal } = context signal?.throwIfAborted() logger.info(`Starting Fal.ai generation with model: ${model}`, { requestId }) - const { endpoint, body } = buildFalVideoRequest(input) + const { endpoint, imageField, body } = buildFalVideoRequest(input) + if (input.visualReference && imageField) { + body[imageField] = await readReferenceImageDataUri( + input.visualReference, + context, + 'Fal.ai start frame image' + ) + signal?.throwIfAborted() + } const createResponse = await fetch(`https://queue.fal.run/${endpoint}`, { method: 'POST', redirect: 'error', diff --git a/apps/sim/lib/internal/video/fal-request.ts b/apps/sim/lib/internal/video/fal-request.ts index 5305ede98fd..19b5708c3df 100644 --- a/apps/sim/lib/internal/video/fal-request.ts +++ b/apps/sim/lib/internal/video/fal-request.ts @@ -1,12 +1,14 @@ import { getErrorMessage } from '@sim/utils/errors' import type { VideoGenerationInput } from '@/lib/internal/video/client' import { assertNoCustomModelSettings, parseCustomModelId } from '@/lib/media/custom-model' -import { getFalVideoModel } from '@/lib/media/fal-video-catalog' +import { getFalVideoModel, getFalVideoVariant } from '@/lib/media/fal-video-catalog' type FalAIDurationFormat = 'number' | 'seconds' | 'string' interface FalAIModelConfig { endpoint: string + /** Request field that carries the start frame on an image-to-video endpoint. */ + imageField?: string durationFormat?: FalAIDurationFormat durationOptions?: readonly number[] supportsAspectRatio?: boolean @@ -82,9 +84,11 @@ export function isCustomFalVideoModel(id: string | undefined): boolean { ) } -function getFalModelConfig(id: string): FalAIModelConfig | undefined { - const model = getFalVideoModel(id) +/** Only catalog models with an image-to-video endpoint take a start frame. */ +function getFalModelConfig(id: string, withImage: boolean): FalAIModelConfig | undefined { + const model = getFalVideoVariant(id, withImage) if (!model) { + if (withImage) return undefined if (Object.hasOwn(LEGACY_FALAI_MODELS, id)) return LEGACY_FALAI_MODELS[id] if (!isCustomFalVideoModel(id)) return undefined return { endpoint: parseCustomModelId(id, 'Fal.ai'), custom: true } @@ -92,6 +96,7 @@ function getFalModelConfig(id: string): FalAIModelConfig | undefined { const sample = model.durations?.[0] return { endpoint: model.endpoint, + imageField: model.imageField, durationFormat: sample === undefined ? undefined @@ -131,14 +136,19 @@ function formatAllowedValues(allowed: readonly (number | string)[]): string { export function getFalAIValidationError(input: VideoGenerationInput): string | undefined { const { model, prompt, duration, aspectRatio, resolution } = input if (!model) return 'Model is required for Fal.ai provider' + const withImage = Boolean(input.visualReference) let modelConfig: FalAIModelConfig | undefined + let promptOnlyConfig: FalAIModelConfig | undefined try { - modelConfig = getFalModelConfig(model) + modelConfig = getFalModelConfig(model, withImage) + if (!modelConfig && withImage) promptOnlyConfig = getFalModelConfig(model, false) } catch (error) { return getErrorMessage(error) } if (!modelConfig) { - return `Unknown Fal.ai model: ${model}` + return promptOnlyConfig + ? `Fal.ai model ${model} does not accept a start frame image` + : `Unknown Fal.ai model: ${model}` } if (modelConfig.custom) { if (input.useHostedCostTracking) @@ -195,10 +205,13 @@ export function getFalAIValidationError(input: VideoGenerationInput): string | u return undefined } -/** Builds the provider request without submitting or billing a generation. */ +/** + * Builds the provider request without submitting or billing a generation. With a start frame the + * request targets the model's image-to-video endpoint, and the caller sets `imageField` to the image. + */ export function buildFalVideoRequest(input: VideoGenerationInput) { const { model, prompt, duration, aspectRatio, resolution, promptOptimizer, generateAudio } = input - const modelConfig = model ? getFalModelConfig(model) : undefined + const modelConfig = model ? getFalModelConfig(model, Boolean(input.visualReference)) : undefined if (!modelConfig) { throw new Error(`Unknown Fal.ai model: ${model}`) } @@ -226,5 +239,5 @@ export function buildFalVideoRequest(input: VideoGenerationInput) { requestBody[modelConfig.audioField] = generateAudio } - return { endpoint: modelConfig.endpoint, body: requestBody } + return { endpoint: modelConfig.endpoint, imageField: modelConfig.imageField, body: requestBody } } diff --git a/apps/sim/lib/internal/video/operations.test.ts b/apps/sim/lib/internal/video/operations.test.ts index 5cdd203f7b4..d2af720dad2 100644 --- a/apps/sim/lib/internal/video/operations.test.ts +++ b/apps/sim/lib/internal/video/operations.test.ts @@ -71,27 +71,30 @@ describe('executeVideoOperation', () => { }) }) - it('fails opaque provenance before inspecting or downloading a Runway file', async () => { - mocks.validateOpaqueModelInputProvenance.mockReturnValue({ - success: false, - error: 'Model input provenance is unavailable', - status: 400, - }) + it.each(['runway', 'falai'] as const)( + 'fails opaque provenance before inspecting or downloading a %s file', + async (provider) => { + mocks.validateOpaqueModelInputProvenance.mockReturnValue({ + success: false, + error: 'Model input provenance is unavailable', + status: 400, + }) - await expect( - executeVideoOperation( - { - provider: 'runway', - apiKey: 'key', - prompt: 'A cinematic sunrise', - visualReference: file, - }, - { headers: new Headers(), requestId: 'request-1', userId: 'user-1' } - ) - ).rejects.toMatchObject({ status: 400, message: 'Model input provenance is unavailable' }) - expect(mockAssertToolFileAccess).not.toHaveBeenCalled() - expect(mocks.generateVideo).not.toHaveBeenCalled() - }) + await expect( + executeVideoOperation( + { + provider, + apiKey: 'key', + prompt: 'A cinematic sunrise', + visualReference: file, + }, + { headers: new Headers(), requestId: 'request-1', userId: 'user-1' } + ) + ).rejects.toMatchObject({ status: 400, message: 'Model input provenance is unavailable' }) + expect(mockAssertToolFileAccess).not.toHaveBeenCalled() + expect(mocks.generateVideo).not.toHaveBeenCalled() + } + ) it('fails closed for model-unsafe workspace files', async () => { mockIsModelSafeWorkspaceFileKey.mockResolvedValue(false) diff --git a/apps/sim/lib/internal/video/operations.ts b/apps/sim/lib/internal/video/operations.ts index a18a4858aa1..6b743f193dc 100644 --- a/apps/sim/lib/internal/video/operations.ts +++ b/apps/sim/lib/internal/video/operations.ts @@ -72,7 +72,7 @@ function validateModelInputProvenance( input: VideoOperationInput, context: VideoOperationContext ): void { - if (input.provider !== 'runway') return + if (input.provider !== 'runway' && !input.visualReference) return const provenance = validateOpaqueModelInputProvenance({ headers: context.headers, payload: input, diff --git a/apps/sim/lib/media/fal-video-catalog.ts b/apps/sim/lib/media/fal-video-catalog.ts index 6ba24ec448d..55f23721260 100644 --- a/apps/sim/lib/media/fal-video-catalog.ts +++ b/apps/sim/lib/media/fal-video-catalog.ts @@ -1,9 +1,7 @@ import models from '@/lib/media/fal-video-models.json' -interface FalVideoModel { - label: string - /** Existing workflows store these model IDs instead of the fal endpoint. */ - legacyId?: string +/** Request settings one fal endpoint accepts. */ +export interface FalVideoSettings { durations?: readonly (number | string)[] aspectRatios?: readonly string[] resolutions?: readonly string[] @@ -14,11 +12,20 @@ interface FalVideoModel { maxDurationByResolution?: Readonly> } +interface FalVideoModel extends FalVideoSettings { + label: string + /** Existing workflows store these model IDs instead of the fal endpoint. */ + legacyId?: string + /** The same model's image-to-video endpoint, used when a start frame is attached. */ + image?: FalVideoSettings & { endpoint: string; imageField: string } +} + const catalog: Record = models /** * Prompt-to-video endpoints from fal's active text-to-video catalog and documented unlisted models. * A model must accept a prompt without media or a custom adapter and return a downloadable video. + * `image` names the model's image-to-video sibling, which takes the same prompt plus a start frame. * Validate catalog updates with `apps/sim/scripts/validate-fal-video-models.ts`. */ export const falVideoModels = Object.entries(catalog).map(([endpoint, spec]) => ({ @@ -35,3 +42,19 @@ for (const model of falVideoModels) { export function getFalVideoModel(id: unknown) { return typeof id === 'string' ? byId.get(id) : undefined } + +/** + * Resolves the endpoint and settings a request uses: the text-to-video endpoint for a prompt, + * or the image-to-video sibling when a start frame is attached. Undefined when the model has no + * endpoint for that input. + */ +export function getFalVideoVariant( + id: unknown, + withImage: boolean +): (FalVideoSettings & { endpoint: string; imageField?: string }) | undefined { + const model = getFalVideoModel(id) + if (!model) return undefined + if (withImage) return model.image + const { label, legacyId, image, modelId, ...settings } = model + return settings +} diff --git a/apps/sim/lib/media/fal-video-models.json b/apps/sim/lib/media/fal-video-models.json index 7ffe1e15344..3034dbde5b6 100644 --- a/apps/sim/lib/media/fal-video-models.json +++ b/apps/sim/lib/media/fal-video-models.json @@ -3,13 +3,25 @@ "label": "Happy Horse", "durations": [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], "aspectRatios": ["16:9", "9:16", "1:1", "4:3", "3:4"], - "resolutions": ["720p", "1080p"] + "resolutions": ["720p", "1080p"], + "image": { + "endpoint": "alibaba/happy-horse/image-to-video", + "imageField": "image_url", + "durations": [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + "resolutions": ["720p", "1080p"] + } }, "alibaba/happy-horse/v1.1/text-to-video": { "label": "Happy Horse 1.1", "durations": [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], "aspectRatios": ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "9:21", "5:4", "4:5"], - "resolutions": ["720p", "1080p"] + "resolutions": ["720p", "1080p"], + "image": { + "endpoint": "alibaba/happy-horse/v1.1/image-to-video", + "imageField": "image_url", + "durations": [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + "resolutions": ["720p", "1080p"] + } }, "alibaba/wan-3.0-prime/text-to-video": { "label": "Wan 3.0 Prime", @@ -19,7 +31,18 @@ ], "aspectRatios": ["adaptive", "16:9", "4:3", "1:1", "3:4", "9:16"], "resolutions": ["480p", "720p", "1080p"], - "audioField": "audio" + "audioField": "audio", + "image": { + "endpoint": "alibaba/wan-3.0-prime/image-to-video", + "imageField": "start_image_url", + "durations": [ + 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, + 27, 28, 29, 30 + ], + "aspectRatios": ["adaptive", "16:9", "4:3", "1:1", "3:4", "9:16"], + "resolutions": ["480p", "720p", "1080p"], + "audioField": "audio" + } }, "alibaba/wan-3.0/text-to-video": { "label": "Wan 3.0", @@ -29,27 +52,61 @@ ], "aspectRatios": ["adaptive", "16:9", "4:3", "1:1", "3:4", "9:16"], "resolutions": ["480p", "720p", "1080p"], - "audioField": "audio" + "audioField": "audio", + "image": { + "endpoint": "alibaba/wan-3.0/image-to-video", + "imageField": "start_image_url", + "durations": [ + 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, + 27, 28, 29, 30 + ], + "aspectRatios": ["adaptive", "16:9", "4:3", "1:1", "3:4", "9:16"], + "resolutions": ["480p", "720p", "1080p"], + "audioField": "audio" + } }, "blackforestlabs/flux-3-max/text-to-video": { "label": "FLUX 3 Max", "durations": [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], "aspectRatios": ["auto", "21:9", "2:1", "16:9", "4:3", "1:1", "3:4", "9:16"], "resolutions": ["720p", "1080p"], - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "blackforestlabs/flux-3-max/image-to-video", + "imageField": "image_url", + "durations": [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], + "aspectRatios": ["auto", "21:9", "2:1", "16:9", "4:3", "1:1", "3:4", "9:16"], + "resolutions": ["720p", "1080p"], + "audioField": "generate_audio" + } }, "blackforestlabs/flux-3/text-to-video": { "label": "Flux 3", "durations": [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], "aspectRatios": ["auto", "21:9", "2:1", "16:9", "4:3", "1:1", "3:4", "9:16"], "resolutions": ["720p", "1080p", "1440p", "2160p"], - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "blackforestlabs/flux-3/image-to-video", + "imageField": "image_url", + "durations": [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], + "aspectRatios": ["auto", "21:9", "2:1", "16:9", "4:3", "1:1", "3:4", "9:16"], + "resolutions": ["720p", "1080p", "1440p", "2160p"], + "audioField": "generate_audio" + } }, "blackforestlabs/flux-3/text-to-video/draft": { "label": "Flux 3 Draft", "durations": [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], "aspectRatios": ["auto", "21:9", "2:1", "16:9", "4:3", "1:1", "3:4", "9:16"], - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "blackforestlabs/flux-3/image-to-video/draft", + "imageField": "image_url", + "durations": [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], + "aspectRatios": ["auto", "21:9", "2:1", "16:9", "4:3", "1:1", "3:4", "9:16"], + "audioField": "generate_audio" + } }, "bytedance/seedance-2.0/fast/text-to-video": { "label": "ByteDance Seedance 2.0 Fast", @@ -57,14 +114,30 @@ "durations": ["4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], "aspectRatios": ["auto", "21:9", "16:9", "4:3", "1:1", "3:4", "9:16"], "resolutions": ["480p", "720p"], - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "bytedance/seedance-2.0/fast/image-to-video", + "imageField": "image_url", + "durations": ["4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], + "aspectRatios": ["auto", "21:9", "16:9", "4:3", "1:1", "3:4", "9:16"], + "resolutions": ["480p", "720p"], + "audioField": "generate_audio" + } }, "bytedance/seedance-2.0/mini/text-to-video": { "label": "Seedance 2.0 Mini", "durations": ["4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], "aspectRatios": ["auto", "21:9", "16:9", "4:3", "1:1", "3:4", "9:16"], "resolutions": ["480p", "720p"], - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "bytedance/seedance-2.0/mini/image-to-video", + "imageField": "image_url", + "durations": ["4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], + "aspectRatios": ["auto", "21:9", "16:9", "4:3", "1:1", "3:4", "9:16"], + "resolutions": ["480p", "720p"], + "audioField": "generate_audio" + } }, "bytedance/seedance-2.0/text-to-video": { "label": "ByteDance Seedance 2.0", @@ -72,14 +145,30 @@ "durations": ["4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], "aspectRatios": ["auto", "21:9", "16:9", "4:3", "1:1", "3:4", "9:16"], "resolutions": ["480p", "720p", "1080p", "4k"], - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "bytedance/seedance-2.0/image-to-video", + "imageField": "image_url", + "durations": ["4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], + "aspectRatios": ["auto", "21:9", "16:9", "4:3", "1:1", "3:4", "9:16"], + "resolutions": ["480p", "720p", "1080p", "4k"], + "audioField": "generate_audio" + } }, "bytedance/seedance-2.0/us/text-to-video": { "label": "Seedance 2.0 US", "durations": ["4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], "aspectRatios": ["auto", "21:9", "16:9", "4:3", "1:1", "3:4", "9:16"], "resolutions": ["480p", "720p", "1080p", "4k"], - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "bytedance/seedance-2.0/us/image-to-video", + "imageField": "image_url", + "durations": ["4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], + "aspectRatios": ["auto", "21:9", "16:9", "4:3", "1:1", "3:4", "9:16"], + "resolutions": ["480p", "720p", "1080p", "4k"], + "audioField": "generate_audio" + } }, "bytedance/seedance-2.5/text-to-video": { "label": "Seedance 2.5", @@ -114,7 +203,43 @@ ], "aspectRatios": ["auto", "21:9", "16:9", "4:3", "1:1", "3:4", "9:16"], "resolutions": ["480p", "720p", "1080p"], - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "bytedance/seedance-2.5/image-to-video", + "imageField": "image_url", + "durations": [ + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30" + ], + "aspectRatios": ["auto"], + "resolutions": ["480p", "720p", "1080p"], + "audioField": "generate_audio" + } }, "bytedance/seedance-2.5/us/text-to-video": { "label": "Seedance 2.5 US", @@ -149,7 +274,43 @@ ], "aspectRatios": ["auto", "21:9", "16:9", "4:3", "1:1", "3:4", "9:16"], "resolutions": ["480p", "720p", "1080p"], - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "bytedance/seedance-2.5/us/image-to-video", + "imageField": "image_url", + "durations": [ + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30" + ], + "aspectRatios": ["auto"], + "resolutions": ["480p", "720p", "1080p"], + "audioField": "generate_audio" + } }, "creatify/boreal": { "label": "Boreal", @@ -162,19 +323,41 @@ "durations": ["4", "5", "6", "7", "8", "9", "10", "11", "12"], "aspectRatios": ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16", "auto"], "resolutions": ["480p", "720p", "1080p"], - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "fal-ai/bytedance/seedance/v1.5/pro/image-to-video", + "imageField": "image_url", + "durations": ["4", "5", "6", "7", "8", "9", "10", "11", "12"], + "aspectRatios": ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16", "auto"], + "resolutions": ["480p", "720p", "1080p"], + "audioField": "generate_audio" + } }, "fal-ai/bytedance/seedance/v1/pro/fast/text-to-video": { "label": "Bytedance Seedance V1 Pro Fast", "durations": ["2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"], "aspectRatios": ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16"], - "resolutions": ["480p", "720p", "1080p"] + "resolutions": ["480p", "720p", "1080p"], + "image": { + "endpoint": "fal-ai/bytedance/seedance/v1/pro/fast/image-to-video", + "imageField": "image_url", + "durations": ["2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"], + "aspectRatios": ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16", "auto"], + "resolutions": ["480p", "720p", "1080p"] + } }, "fal-ai/bytedance/seedance/v1/pro/text-to-video": { "label": "Seedance 1.0 Pro", "durations": ["2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"], "aspectRatios": ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16"], - "resolutions": ["480p", "720p", "1080p"] + "resolutions": ["480p", "720p", "1080p"], + "image": { + "endpoint": "fal-ai/bytedance/seedance/v1/pro/image-to-video", + "imageField": "image_url", + "durations": ["2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"], + "aspectRatios": ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16", "auto"], + "resolutions": ["480p", "720p", "1080p"] + } }, "fal-ai/heygen/avatar5/digital-twin": { "label": "HeyGen Avatar V Digital Twin", @@ -195,7 +378,13 @@ "fal-ai/hunyuan-video-v1.5/text-to-video": { "label": "Hunyuan Video V1.5", "aspectRatios": ["16:9", "9:16"], - "resolutions": ["480p"] + "resolutions": ["480p"], + "image": { + "endpoint": "fal-ai/hunyuan-video-v1.5/image-to-video", + "imageField": "image_url", + "aspectRatios": ["16:9", "9:16"], + "resolutions": ["480p"] + } }, "fal-ai/infinity-star/text-to-video": { "label": "Infinity Star", @@ -205,7 +394,13 @@ "label": "Kandinsky5 Pro", "durations": ["5s"], "aspectRatios": ["3:2", "1:1", "2:3"], - "resolutions": ["512P", "1024P"] + "resolutions": ["512P", "1024P"], + "image": { + "endpoint": "fal-ai/kandinsky5-pro/image-to-video", + "imageField": "image_url", + "durations": ["5s"], + "resolutions": ["512P", "1024P"] + } }, "fal-ai/kandinsky5/text-to-video": { "label": "Kandinsky 5", @@ -222,108 +417,213 @@ "fal-ai/kandinsky6-lite/text-to-video": { "label": "Kandinsky 6.0 Lite", "aspectRatios": ["16:9", "9:16", "1:1", "4:3", "3:4"], - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "fal-ai/kandinsky6-lite/image-to-video", + "imageField": "image_url", + "aspectRatios": ["16:9", "9:16", "1:1", "4:3", "3:4"], + "audioField": "generate_audio" + } }, "fal-ai/kandinsky6-pro/text-to-video": { "label": "Kandinsky 6.0 Pro", "aspectRatios": ["16:9", "9:16", "1:1", "4:3", "3:4"], - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "fal-ai/kandinsky6-pro/image-to-video", + "imageField": "image_url", + "aspectRatios": ["16:9", "9:16", "1:1", "4:3", "3:4"], + "audioField": "generate_audio" + } }, "fal-ai/kling-video/o3/4k/text-to-video": { "label": "Kling O3 4K", "legacyId": "kling-o3-4k", "durations": ["3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], "aspectRatios": ["16:9", "9:16", "1:1"], - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "fal-ai/kling-video/o3/4k/image-to-video", + "imageField": "image_url", + "durations": ["3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], + "audioField": "generate_audio" + } }, "fal-ai/kling-video/o3/pro/text-to-video": { "label": "Kling O3 Pro", "legacyId": "kling-o3-pro", "durations": ["3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], "aspectRatios": ["16:9", "9:16", "1:1"], - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "fal-ai/kling-video/o3/pro/image-to-video", + "imageField": "image_url", + "durations": ["3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], + "audioField": "generate_audio" + } }, "fal-ai/kling-video/o3/standard/text-to-video": { "label": "Kling O3 [Standard]", "durations": ["3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], "aspectRatios": ["16:9", "9:16", "1:1"], - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "fal-ai/kling-video/o3/standard/image-to-video", + "imageField": "image_url", + "durations": ["3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], + "audioField": "generate_audio" + } }, "fal-ai/kling-video/v2.5-turbo/pro/text-to-video": { "label": "Kling v2.5", "durations": ["5", "10"], - "aspectRatios": ["16:9", "9:16", "1:1"] + "aspectRatios": ["16:9", "9:16", "1:1"], + "image": { + "endpoint": "fal-ai/kling-video/v2.5-turbo/pro/image-to-video", + "imageField": "image_url", + "durations": ["5", "10"] + } }, "fal-ai/kling-video/v2.6/pro/text-to-video": { "label": "Kling Video v2.6", "durations": ["5", "10"], "aspectRatios": ["16:9", "9:16", "1:1"], - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "fal-ai/kling-video/v2.6/pro/image-to-video", + "imageField": "start_image_url", + "durations": ["5", "10"], + "audioField": "generate_audio" + } }, "fal-ai/kling-video/v3/4k/text-to-video": { "label": "Kling 3.0 4K", "legacyId": "kling-v3-4k", "durations": ["3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], "aspectRatios": ["16:9", "9:16", "1:1"], - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "fal-ai/kling-video/v3/4k/image-to-video", + "imageField": "start_image_url", + "durations": ["3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], + "audioField": "generate_audio" + } }, "fal-ai/kling-video/v3/pro/text-to-video": { "label": "Kling 3.0 Pro", "legacyId": "kling-v3-pro", "durations": ["3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], "aspectRatios": ["16:9", "9:16", "1:1"], - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "fal-ai/kling-video/v3/pro/image-to-video", + "imageField": "start_image_url", + "durations": ["3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], + "audioField": "generate_audio" + } }, "fal-ai/kling-video/v3/standard/text-to-video": { "label": "Kling Video v3 [Standard]", "durations": ["3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], "aspectRatios": ["16:9", "9:16", "1:1"], - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "fal-ai/kling-video/v3/standard/image-to-video", + "imageField": "start_image_url", + "durations": ["3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], + "audioField": "generate_audio" + } }, "fal-ai/kling-video/v3/turbo/pro/text-to-video": { "label": "Kling Video V3 Turbo Pro", "durations": ["3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], - "aspectRatios": ["16:9", "9:16", "1:1"] + "aspectRatios": ["16:9", "9:16", "1:1"], + "image": { + "endpoint": "fal-ai/kling-video/v3/turbo/pro/image-to-video", + "imageField": "image_url", + "durations": ["3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"] + } }, "fal-ai/kling-video/v3/turbo/standard/text-to-video": { "label": "Kling Video V3 Standard Turbo", "durations": ["3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], - "aspectRatios": ["16:9", "9:16", "1:1"] + "aspectRatios": ["16:9", "9:16", "1:1"], + "image": { + "endpoint": "fal-ai/kling-video/v3/turbo/standard/image-to-video", + "imageField": "image_url", + "durations": ["3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"] + } }, "fal-ai/krea-wan-14b/text-to-video": { "label": "Krea Wan 14b" }, "fal-ai/longcat-video/distilled/text-to-video/480p": { "label": "LongCat Video Distilled 480p", - "aspectRatios": ["16:9", "9:16", "1:1"] + "aspectRatios": ["16:9", "9:16", "1:1"], + "image": { + "endpoint": "fal-ai/longcat-video/distilled/image-to-video/480p", + "imageField": "image_url" + } }, "fal-ai/longcat-video/distilled/text-to-video/720p": { "label": "LongCat Video Distilled 720p", - "aspectRatios": ["16:9", "9:16", "1:1"] + "aspectRatios": ["16:9", "9:16", "1:1"], + "image": { + "endpoint": "fal-ai/longcat-video/distilled/image-to-video/720p", + "imageField": "image_url" + } }, "fal-ai/longcat-video/text-to-video/480p": { "label": "LongCat Video 480p", - "aspectRatios": ["16:9", "9:16", "1:1"] + "aspectRatios": ["16:9", "9:16", "1:1"], + "image": { + "endpoint": "fal-ai/longcat-video/image-to-video/480p", + "imageField": "image_url" + } }, "fal-ai/longcat-video/text-to-video/720p": { "label": "LongCat Video 720p", - "aspectRatios": ["16:9", "9:16", "1:1"] + "aspectRatios": ["16:9", "9:16", "1:1"], + "image": { + "endpoint": "fal-ai/longcat-video/image-to-video/720p", + "imageField": "image_url" + } }, "fal-ai/ltx-2-19b/distilled/text-to-video": { "label": "LTX-2 19B Distilled", - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "fal-ai/ltx-2-19b/distilled/image-to-video", + "imageField": "image_url", + "audioField": "generate_audio" + } }, "fal-ai/ltx-2-19b/text-to-video": { "label": "LTX-2 19B", - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "fal-ai/ltx-2-19b/image-to-video", + "imageField": "image_url", + "audioField": "generate_audio" + } }, "fal-ai/ltx-2.3-22b/distilled/text-to-video": { "label": "LTX-2.3 22B Distilled", - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "fal-ai/ltx-2.3-22b/distilled/image-to-video", + "imageField": "image_url", + "audioField": "generate_audio" + } }, "fal-ai/ltx-2.3-22b/text-to-video": { "label": "LTX-2.3 22B", - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "fal-ai/ltx-2.3-22b/image-to-video", + "imageField": "image_url", + "audioField": "generate_audio" + } }, "fal-ai/ltx-2.3-quality/text-to-video": { "label": "Ltx 2.3 Quality", @@ -335,7 +635,21 @@ "landscape_4_3", "landscape_16_9" ], - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "fal-ai/ltx-2.3-quality/image-to-video", + "imageField": "image_url", + "resolutions": [ + "auto", + "square_hd", + "square", + "portrait_4_3", + "portrait_16_9", + "landscape_4_3", + "landscape_16_9" + ], + "audioField": "generate_audio" + } }, "fal-ai/ltx-2.3/text-to-video": { "label": "LTX 2.3", @@ -343,7 +657,15 @@ "durations": [6, 8, 10], "aspectRatios": ["16:9", "9:16"], "resolutions": ["1080p", "1440p", "2160p"], - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "fal-ai/ltx-2.3/image-to-video", + "imageField": "image_url", + "durations": [6, 8, 10], + "aspectRatios": ["auto", "16:9", "9:16"], + "resolutions": ["1080p", "1440p", "2160p"], + "audioField": "generate_audio" + } }, "fal-ai/ltx-2.3/text-to-video/fast": { "label": "LTX 2.3 Fast", @@ -355,32 +677,74 @@ "maxDurationByResolution": { "1440p": 10, "2160p": 10 + }, + "image": { + "endpoint": "fal-ai/ltx-2.3/image-to-video/fast", + "imageField": "image_url", + "durations": [6, 8, 10, 12, 14, 16, 18, 20], + "aspectRatios": ["auto", "16:9", "9:16"], + "resolutions": ["1080p", "1440p", "2160p"], + "audioField": "generate_audio", + "maxDurationByResolution": { + "1440p": 10, + "2160p": 10 + } } }, "fal-ai/ltx-video": { - "label": "LTX Video (preview, research license)" + "label": "LTX Video (preview, research license)", + "image": { + "endpoint": "fal-ai/ltx-video/image-to-video", + "imageField": "image_url" + } }, "fal-ai/ltx-video-13b-distilled": { "label": "LTX Video-0.9.7 13B Distilled", "aspectRatios": ["9:16", "1:1", "16:9"], - "resolutions": ["480p", "720p"] + "resolutions": ["480p", "720p"], + "image": { + "endpoint": "fal-ai/ltx-video-13b-distilled/image-to-video", + "imageField": "image_url", + "aspectRatios": ["9:16", "1:1", "16:9", "auto"], + "resolutions": ["480p", "720p"] + } }, "fal-ai/ltxv-13b-098-distilled": { "label": "LTX-Video 13B 0.9.8 Distilled", "aspectRatios": ["9:16", "1:1", "16:9"], - "resolutions": ["480p", "720p"] + "resolutions": ["480p", "720p"], + "image": { + "endpoint": "fal-ai/ltxv-13b-098-distilled/image-to-video", + "imageField": "image_url", + "aspectRatios": ["9:16", "1:1", "16:9", "auto"], + "resolutions": ["480p", "720p"] + } }, "fal-ai/luma-dream-machine/ray-2": { "label": "Luma Ray 2", "durations": ["5s", "9s"], "aspectRatios": ["16:9", "9:16", "4:3", "3:4", "21:9", "9:21"], - "resolutions": ["540p", "720p", "1080p"] + "resolutions": ["540p", "720p", "1080p"], + "image": { + "endpoint": "fal-ai/luma-dream-machine/ray-2/image-to-video", + "imageField": "image_url", + "durations": ["5s", "9s"], + "aspectRatios": ["16:9", "9:16", "4:3", "3:4", "21:9", "9:21"], + "resolutions": ["540p", "720p", "1080p"] + } }, "fal-ai/luma-dream-machine/ray-2-flash": { "label": "Luma Ray 2 Flash", "durations": ["5s", "9s"], "aspectRatios": ["16:9", "9:16", "4:3", "3:4", "21:9", "9:21"], - "resolutions": ["540p", "720p", "1080p"] + "resolutions": ["540p", "720p", "1080p"], + "image": { + "endpoint": "fal-ai/luma-dream-machine/ray-2-flash/image-to-video", + "imageField": "image_url", + "durations": ["5s", "9s"], + "aspectRatios": ["16:9", "9:16", "4:3", "3:4", "21:9", "9:21"], + "resolutions": ["540p", "720p", "1080p"] + } }, "fal-ai/magi-distilled": { "label": "MAGI-1 (Distilled)", @@ -389,45 +753,93 @@ }, "fal-ai/minimax/hailuo-02/pro/text-to-video": { "label": "MiniMax Hailuo 02 [Pro]", - "promptOptimizer": true + "promptOptimizer": true, + "image": { + "endpoint": "fal-ai/minimax/hailuo-02/pro/image-to-video", + "imageField": "image_url", + "promptOptimizer": true + } }, "fal-ai/minimax/hailuo-02/standard/text-to-video": { "label": "MiniMax Hailuo 02 [Standard]", "durations": ["6", "10"], - "promptOptimizer": true + "promptOptimizer": true, + "image": { + "endpoint": "fal-ai/minimax/hailuo-02/standard/image-to-video", + "imageField": "image_url", + "durations": ["6", "10"], + "resolutions": ["512P", "768P"], + "promptOptimizer": true + } }, "fal-ai/minimax/hailuo-2.3/pro/text-to-video": { "label": "MiniMax Hailuo 2.3 Pro", "legacyId": "minimax-hailuo-2.3-pro", - "promptOptimizer": true + "promptOptimizer": true, + "image": { + "endpoint": "fal-ai/minimax/hailuo-2.3/pro/image-to-video", + "imageField": "image_url", + "promptOptimizer": true + } }, "fal-ai/minimax/hailuo-2.3/standard/text-to-video": { "label": "MiniMax Hailuo 2.3 Standard", "legacyId": "minimax-hailuo-2.3-standard", "durations": ["6", "10"], - "promptOptimizer": true + "promptOptimizer": true, + "image": { + "endpoint": "fal-ai/minimax/hailuo-2.3/standard/image-to-video", + "imageField": "image_url", + "durations": ["6", "10"], + "promptOptimizer": true + } }, "fal-ai/minimax/video-01": { "label": "MiniMax (Hailuo AI) Video 01", - "promptOptimizer": true + "promptOptimizer": true, + "image": { + "endpoint": "fal-ai/minimax/video-01/image-to-video", + "imageField": "image_url", + "promptOptimizer": true + } }, "fal-ai/minimax/video-01-director": { "label": "MiniMax (Hailuo AI) Video 01 Director", - "promptOptimizer": true + "promptOptimizer": true, + "image": { + "endpoint": "fal-ai/minimax/video-01-director/image-to-video", + "imageField": "image_url", + "promptOptimizer": true + } }, "fal-ai/minimax/video-01-live": { "label": "MiniMax (Hailuo AI) Video 01 Live", - "promptOptimizer": true + "promptOptimizer": true, + "image": { + "endpoint": "fal-ai/minimax/video-01-live/image-to-video", + "imageField": "image_url", + "promptOptimizer": true + } }, "fal-ai/ovi": { "label": "Ovi", - "resolutions": ["512x992", "992x512", "960x512", "512x960", "720x720", "448x1120", "1120x448"] + "resolutions": ["512x992", "992x512", "960x512", "512x960", "720x720", "448x1120", "1120x448"], + "image": { + "endpoint": "fal-ai/ovi/image-to-video", + "imageField": "image_url" + } }, "fal-ai/pixverse/c1/text-to-video": { "label": "PixVerse C1", "durations": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], "aspectRatios": ["16:9", "4:3", "1:1", "3:4", "9:16", "2:3", "3:2", "21:9"], - "resolutions": ["360p", "540p", "720p", "1080p"] + "resolutions": ["360p", "540p", "720p", "1080p"], + "image": { + "endpoint": "fal-ai/pixverse/c1/image-to-video", + "imageField": "image_url", + "durations": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + "resolutions": ["360p", "540p", "720p", "1080p"] + } }, "fal-ai/pixverse/v3.5/text-to-video": { "label": "PixVerse V3.5", @@ -436,12 +848,26 @@ "resolutions": ["360p", "540p", "720p", "1080p"], "maxDurationByResolution": { "1080p": 5 + }, + "image": { + "endpoint": "fal-ai/pixverse/v3.5/image-to-video", + "imageField": "image_url", + "durations": ["5", "8"], + "resolutions": ["360p", "540p", "720p", "1080p"], + "maxDurationByResolution": { + "1080p": 5 + } } }, "fal-ai/pixverse/v3.5/text-to-video/fast": { "label": "PixVerse V3.5 Fast", "aspectRatios": ["16:9", "4:3", "1:1", "3:4", "9:16"], - "resolutions": ["360p", "540p", "720p"] + "resolutions": ["360p", "540p", "720p"], + "image": { + "endpoint": "fal-ai/pixverse/v3.5/image-to-video/fast", + "imageField": "image_url", + "resolutions": ["360p", "540p", "720p"] + } }, "fal-ai/pixverse/v4.5/text-to-video": { "label": "PixVerse V4.5", @@ -450,12 +876,26 @@ "resolutions": ["360p", "540p", "720p", "1080p"], "maxDurationByResolution": { "1080p": 5 + }, + "image": { + "endpoint": "fal-ai/pixverse/v4.5/image-to-video", + "imageField": "image_url", + "durations": ["5", "8"], + "resolutions": ["360p", "540p", "720p", "1080p"], + "maxDurationByResolution": { + "1080p": 5 + } } }, "fal-ai/pixverse/v4.5/text-to-video/fast": { "label": "PixVerse V4.5 Fast", "aspectRatios": ["16:9", "4:3", "1:1", "3:4", "9:16"], - "resolutions": ["360p", "540p", "720p"] + "resolutions": ["360p", "540p", "720p"], + "image": { + "endpoint": "fal-ai/pixverse/v4.5/image-to-video/fast", + "imageField": "image_url", + "resolutions": ["360p", "540p", "720p"] + } }, "fal-ai/pixverse/v4/text-to-video": { "label": "PixVerse V4", @@ -464,12 +904,26 @@ "resolutions": ["360p", "540p", "720p", "1080p"], "maxDurationByResolution": { "1080p": 5 + }, + "image": { + "endpoint": "fal-ai/pixverse/v4/image-to-video", + "imageField": "image_url", + "durations": ["5", "8"], + "resolutions": ["360p", "540p", "720p", "1080p"], + "maxDurationByResolution": { + "1080p": 5 + } } }, "fal-ai/pixverse/v4/text-to-video/fast": { "label": "PixVerse V4 Fast", "aspectRatios": ["16:9", "4:3", "1:1", "3:4", "9:16"], - "resolutions": ["360p", "540p", "720p"] + "resolutions": ["360p", "540p", "720p"], + "image": { + "endpoint": "fal-ai/pixverse/v4/image-to-video/fast", + "imageField": "image_url", + "resolutions": ["360p", "540p", "720p"] + } }, "fal-ai/pixverse/v5.5/text-to-video": { "label": "PixVerse V5.5", @@ -478,6 +932,15 @@ "resolutions": ["360p", "540p", "720p", "1080p"], "maxDurationByResolution": { "1080p": 8 + }, + "image": { + "endpoint": "fal-ai/pixverse/v5.5/image-to-video", + "imageField": "image_url", + "durations": ["5", "8", "10"], + "resolutions": ["360p", "540p", "720p", "1080p"], + "maxDurationByResolution": { + "1080p": 8 + } } }, "fal-ai/pixverse/v5.6/text-to-video": { @@ -487,6 +950,15 @@ "resolutions": ["360p", "540p", "720p", "1080p"], "maxDurationByResolution": { "1080p": 8 + }, + "image": { + "endpoint": "fal-ai/pixverse/v5.6/image-to-video", + "imageField": "image_url", + "durations": ["5", "8", "10"], + "resolutions": ["360p", "540p", "720p", "1080p"], + "maxDurationByResolution": { + "1080p": 8 + } } }, "fal-ai/pixverse/v5/text-to-video": { @@ -496,13 +968,28 @@ "resolutions": ["360p", "540p", "720p", "1080p"], "maxDurationByResolution": { "1080p": 5 + }, + "image": { + "endpoint": "fal-ai/pixverse/v5/image-to-video", + "imageField": "image_url", + "durations": ["5", "8"], + "resolutions": ["360p", "540p", "720p", "1080p"], + "maxDurationByResolution": { + "1080p": 5 + } } }, "fal-ai/pixverse/v6/text-to-video": { "label": "PixVerse V6", "durations": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], "aspectRatios": ["16:9", "4:3", "1:1", "3:4", "9:16", "2:3", "3:2", "21:9"], - "resolutions": ["360p", "540p", "720p", "1080p"] + "resolutions": ["360p", "540p", "720p", "1080p"], + "image": { + "endpoint": "fal-ai/pixverse/v6/image-to-video", + "imageField": "image_url", + "durations": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + "resolutions": ["360p", "540p", "720p", "1080p"] + } }, "fal-ai/veo3.1": { "label": "Google Veo 3.1", @@ -510,7 +997,15 @@ "durations": ["4s", "6s", "8s"], "aspectRatios": ["16:9", "9:16"], "resolutions": ["720p", "1080p", "4k"], - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "fal-ai/veo3.1/image-to-video", + "imageField": "image_url", + "durations": ["4s", "6s", "8s"], + "aspectRatios": ["auto", "16:9", "9:16"], + "resolutions": ["720p", "1080p", "4k"], + "audioField": "generate_audio" + } }, "fal-ai/veo3.1/fast": { "label": "Google Veo 3.1 Fast", @@ -518,19 +1013,40 @@ "durations": ["4s", "6s", "8s"], "aspectRatios": ["16:9", "9:16"], "resolutions": ["720p", "1080p", "4k"], - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "fal-ai/veo3.1/fast/image-to-video", + "imageField": "image_url", + "durations": ["4s", "6s", "8s"], + "aspectRatios": ["auto", "16:9", "9:16"], + "resolutions": ["720p", "1080p", "4k"], + "audioField": "generate_audio" + } }, "fal-ai/veo3.1/lite": { "label": "Google Veo 3.1 Lite", "durations": ["4s", "6s", "8s"], "aspectRatios": ["16:9", "9:16"], "resolutions": ["720p", "1080p"], - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "fal-ai/veo3.1/lite/image-to-video", + "imageField": "image_url", + "durations": ["4s", "6s", "8s"], + "aspectRatios": ["auto", "16:9", "9:16"], + "resolutions": ["720p", "1080p"], + "audioField": "generate_audio" + } }, "fal-ai/vidu/q1/text-to-video": { "label": "Vidu Q1", "maxPromptLength": 1500, - "aspectRatios": ["16:9", "9:16", "1:1"] + "aspectRatios": ["16:9", "9:16", "1:1"], + "image": { + "endpoint": "fal-ai/vidu/q1/image-to-video", + "imageField": "image_url", + "maxPromptLength": 1500 + } }, "fal-ai/vidu/q2/text-to-video": { "label": "Vidu Q2", @@ -543,33 +1059,69 @@ "durations": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], "aspectRatios": ["16:9", "9:16", "4:3", "3:4", "1:1"], "resolutions": ["360p", "540p", "720p", "1080p"], - "audioField": "audio" + "audioField": "audio", + "image": { + "endpoint": "fal-ai/vidu/q3/image-to-video", + "imageField": "image_url", + "durations": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], + "resolutions": ["360p", "540p", "720p", "1080p"], + "audioField": "audio" + } }, "fal-ai/vidu/q3/text-to-video/turbo": { "label": "Vidu Q3 Turbo", "durations": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], "aspectRatios": ["16:9", "9:16", "4:3", "3:4", "1:1"], "resolutions": ["360p", "540p", "720p", "1080p"], - "audioField": "audio" + "audioField": "audio", + "image": { + "endpoint": "fal-ai/vidu/q3/image-to-video/turbo", + "imageField": "image_url", + "durations": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], + "resolutions": ["360p", "540p", "720p", "1080p"], + "audioField": "audio" + } }, "fal-ai/wan-25-preview/text-to-video": { "label": "Wan 2.5", "durations": ["5", "10"], "aspectRatios": ["16:9", "9:16", "1:1"], - "resolutions": ["480p", "720p", "1080p"] + "resolutions": ["480p", "720p", "1080p"], + "image": { + "endpoint": "fal-ai/wan-25-preview/image-to-video", + "imageField": "image_url", + "durations": ["5", "10"], + "resolutions": ["480p", "720p", "1080p"] + } }, "fal-ai/wan-pro/text-to-video": { - "label": "Wan-2.1 Pro" + "label": "Wan-2.1 Pro", + "image": { + "endpoint": "fal-ai/wan-pro/image-to-video", + "imageField": "image_url" + } }, "fal-ai/wan-t2v-lora": { "label": "Wan-2.1 with LoRAs", "aspectRatios": ["9:16", "16:9"], - "resolutions": ["480p", "580p", "720p"] + "resolutions": ["480p", "580p", "720p"], + "image": { + "endpoint": "fal-ai/wan-i2v-lora", + "imageField": "image_url", + "aspectRatios": ["auto", "16:9", "9:16", "1:1"], + "resolutions": ["480p", "720p"] + } }, "fal-ai/wan/v2.2-5b/text-to-video": { "label": "Wan v2.2 5B", "aspectRatios": ["16:9", "9:16", "1:1"], - "resolutions": ["580p", "720p"] + "resolutions": ["580p", "720p"], + "image": { + "endpoint": "fal-ai/wan/v2.2-5b/image-to-video", + "imageField": "image_url", + "aspectRatios": ["auto", "16:9", "9:16", "1:1"], + "resolutions": ["580p", "720p"] + } }, "fal-ai/wan/v2.2-5b/text-to-video/distill": { "label": "Wan 2.2 5B Distilled", @@ -584,41 +1136,84 @@ "fal-ai/wan/v2.2-a14b/text-to-video": { "label": "Wan-2.2 A14B", "aspectRatios": ["16:9", "9:16", "1:1"], - "resolutions": ["480p", "580p", "720p"] + "resolutions": ["480p", "580p", "720p"], + "image": { + "endpoint": "fal-ai/wan/v2.2-a14b/image-to-video", + "imageField": "image_url", + "aspectRatios": ["auto", "16:9", "9:16", "1:1"], + "resolutions": ["480p", "580p", "720p"] + } }, "fal-ai/wan/v2.2-a14b/text-to-video/lora": { "label": "Wan-2.2 A14B with LoRAs", "aspectRatios": ["16:9", "9:16", "1:1"], - "resolutions": ["480p", "580p", "720p"] + "resolutions": ["480p", "580p", "720p"], + "image": { + "endpoint": "fal-ai/wan/v2.2-a14b/image-to-video/lora", + "imageField": "image_url", + "aspectRatios": ["auto", "16:9", "9:16", "1:1"], + "resolutions": ["480p", "580p", "720p"] + } }, "fal-ai/wan/v2.2-a14b/text-to-video/turbo": { "label": "WAN 2.2 A14B Turbo", "legacyId": "wan-2.2-a14b-turbo", "aspectRatios": ["16:9", "9:16", "1:1"], - "resolutions": ["480p", "580p", "720p"] + "resolutions": ["480p", "580p", "720p"], + "image": { + "endpoint": "fal-ai/wan/v2.2-a14b/image-to-video/turbo", + "imageField": "image_url", + "aspectRatios": ["auto", "16:9", "9:16", "1:1"], + "resolutions": ["480p", "580p", "720p"] + } }, "fal-ai/wan/v2.7/text-to-video": { "label": "Wan 2.7", "durations": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], "aspectRatios": ["16:9", "9:16", "1:1", "4:3", "3:4"], - "resolutions": ["720p", "1080p"] + "resolutions": ["720p", "1080p"], + "image": { + "endpoint": "fal-ai/wan/v2.7/image-to-video", + "imageField": "image_url", + "durations": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + "resolutions": ["720p", "1080p"] + } }, "google/gemini-omni-flash": { "label": "Gemini Omni Flash", "durations": [3, 4, 5, 6, 7, 8, 9, 10], - "aspectRatios": ["16:9", "9:16"] + "aspectRatios": ["16:9", "9:16"], + "image": { + "endpoint": "google/gemini-omni-flash/image-to-video", + "imageField": "image_url", + "durations": [3, 4, 5, 6, 7, 8, 9, 10], + "aspectRatios": ["16:9", "9:16"] + } }, "google/gemini-omni-flash/v1.1/text-to-video": { "label": "Gemini Omni Flash 1.1", "durations": [3, 4, 5, 6, 7, 8, 9, 10], "aspectRatios": ["16:9", "9:16"], - "resolutions": ["360p", "720p", "1080p", "4k"] + "resolutions": ["360p", "720p", "1080p", "4k"], + "image": { + "endpoint": "google/gemini-omni-flash/v1.1/image-to-video", + "imageField": "image_url", + "durations": [3, 4, 5, 6, 7, 8, 9, 10], + "aspectRatios": ["16:9", "9:16"], + "resolutions": ["360p", "720p", "1080p", "4k"] + } }, "heygen/heygen-video/text-to-video": { "label": "HeyGen Video", "durations": [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], "aspectRatios": ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16"], - "resolutions": ["480p", "768p", "1080p", "2k"] + "resolutions": ["480p", "768p", "1080p", "2k"], + "image": { + "endpoint": "heygen/heygen-video/image-to-video", + "imageField": "image_url", + "durations": [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + "resolutions": ["480p", "768p", "1080p", "2k"] + } }, "kling/v4/text-to-video": { "label": "Kling 4.0", @@ -627,7 +1222,16 @@ 28, 29, 30 ], "aspectRatios": ["16:9", "9:16", "1:1", "21:9"], - "resolutions": ["720p", "1080p"] + "resolutions": ["720p", "1080p"], + "image": { + "endpoint": "kling/v4/image-to-video", + "imageField": "start_image_url", + "durations": [ + 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, + 28, 29, 30 + ], + "resolutions": ["720p", "1080p"] + } }, "lightricks/ltx-2.5/text-to-video/fast": { "label": "LTX 2.5 Fast", @@ -638,6 +1242,18 @@ "maxDurationByResolution": { "1440p": 10, "2160p": 10 + }, + "image": { + "endpoint": "lightricks/ltx-2.5/image-to-video/fast", + "imageField": "image_url", + "durations": [6, 8, 10, 12, 14, 16, 18, 20], + "aspectRatios": ["auto", "16:9", "9:16"], + "resolutions": ["720p", "1080p", "1440p", "2160p"], + "audioField": "generate_audio", + "maxDurationByResolution": { + "1440p": 10, + "2160p": 10 + } } }, "lightricks/ltx-2.5/text-to-video/pro": { @@ -645,13 +1261,28 @@ "durations": [6, 8, 10], "aspectRatios": ["16:9", "9:16"], "resolutions": ["720p", "1080p"], - "audioField": "generate_audio" + "audioField": "generate_audio", + "image": { + "endpoint": "lightricks/ltx-2.5/image-to-video/pro", + "imageField": "image_url", + "durations": [6, 8, 10], + "aspectRatios": ["auto", "16:9", "9:16"], + "resolutions": ["720p", "1080p"], + "audioField": "generate_audio" + } }, "luma/agent/ray/v3.2/text-to-video": { "label": "Luma Ray 3.2", "durations": ["5s", "10s"], "aspectRatios": ["3:4", "4:3", "1:1", "9:16", "16:9", "21:9"], - "resolutions": ["540p", "720p", "1080p"] + "resolutions": ["540p", "720p", "1080p"], + "image": { + "endpoint": "luma/agent/ray/v3.2/image-to-video", + "imageField": "image_url", + "durations": ["5s", "10s"], + "aspectRatios": ["3:4", "4:3", "1:1", "9:16", "16:9", "21:9"], + "resolutions": ["540p", "720p", "1080p"] + } }, "minimax/h3-max-turbo/text-to-video": { "label": "H3 Max Turbo", @@ -660,7 +1291,16 @@ }, "durations": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], "aspectRatios": ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16"], - "resolutions": ["480P", "768P", "1080P"] + "resolutions": ["480P", "768P", "1080P"], + "image": { + "endpoint": "minimax/h3-max-turbo/image-to-video", + "imageField": "image_url", + "durations": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + "resolutions": ["480P", "768P", "1080P"], + "requestDefaults": { + "prompt_expansion_mode": "balanced" + } + } }, "minimax/h3-max/styles/16bit-pixel": { "label": "H3 Max 16-bit Pixel", @@ -694,40 +1334,85 @@ }, "durations": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], "aspectRatios": ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16"], - "resolutions": ["480P", "768P", "1080P"] + "resolutions": ["480P", "768P", "1080P"], + "image": { + "endpoint": "minimax/h3-max/image-to-video", + "imageField": "image_url", + "durations": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + "resolutions": ["480P", "768P", "1080P"], + "requestDefaults": { + "prompt_expansion_mode": "balanced" + } + } }, "minimax/h3/text-to-video": { "label": "MiniMax H3", "durations": [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], "aspectRatios": ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16"], - "resolutions": ["480P", "768P", "2K", "4K"] + "resolutions": ["480P", "768P", "2K", "4K"], + "image": { + "endpoint": "minimax/h3/image-to-video", + "imageField": "image_url", + "durations": [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + "resolutions": ["480P", "768P", "2K", "4K"] + } }, "moonvalley/marey/t2v": { "label": "Marey Realism V1.5", - "durations": ["5s", "10s"] + "durations": ["5s", "10s"], + "image": { + "endpoint": "moonvalley/marey/i2v", + "imageField": "image_url", + "durations": ["5s", "10s"] + } }, "wan/v2.6/text-to-video": { "label": "Wan v2.6", "durations": ["5", "10", "15"], "aspectRatios": ["16:9", "9:16", "1:1", "4:3", "3:4"], - "resolutions": ["720p", "1080p"] + "resolutions": ["720p", "1080p"], + "image": { + "endpoint": "wan/v2.6/image-to-video", + "imageField": "image_url", + "durations": ["5", "10", "15"], + "resolutions": ["720p", "1080p"] + } }, "xai/grok-imagine-video/text-to-video": { "label": "Grok Imagine Video", "durations": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], "aspectRatios": ["16:9", "4:3", "3:2", "1:1", "2:3", "3:4", "9:16"], - "resolutions": ["480p", "720p"] + "resolutions": ["480p", "720p"], + "image": { + "endpoint": "xai/grok-imagine-video/image-to-video", + "imageField": "image_url", + "durations": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + "aspectRatios": ["auto", "16:9", "4:3", "3:2", "1:1", "2:3", "3:4", "9:16"], + "resolutions": ["480p", "720p"] + } }, "xai/grok-imagine-video/v1.5/lite/text-to-video": { "label": "Grok Imagine Video 1.5 Lite", "durations": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], "aspectRatios": ["16:9", "4:3", "3:2", "1:1", "2:3", "3:4", "9:16"], - "resolutions": ["480p", "720p", "1080p"] + "resolutions": ["480p", "720p", "1080p"], + "image": { + "endpoint": "xai/grok-imagine-video/v1.5/lite/image-to-video", + "imageField": "image_url", + "durations": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + "resolutions": ["480p", "720p", "1080p"] + } }, "xai/grok-imagine-video/v1.5/text-to-video": { "label": "Grok Imagine Video 1.5", "durations": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], "aspectRatios": ["16:9", "4:3", "3:2", "1:1", "2:3", "3:4", "9:16"], - "resolutions": ["480p", "720p", "1080p"] + "resolutions": ["480p", "720p", "1080p"], + "image": { + "endpoint": "xai/grok-imagine-video/v1.5/image-to-video", + "imageField": "image_url", + "durations": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + "resolutions": ["480p", "720p", "1080p"] + } } } diff --git a/apps/sim/lib/tools/falai-pricing.test.ts b/apps/sim/lib/tools/falai-pricing.test.ts index 3b657ebc019..28ca4844e80 100644 --- a/apps/sim/lib/tools/falai-pricing.test.ts +++ b/apps/sim/lib/tools/falai-pricing.test.ts @@ -28,6 +28,8 @@ describe('getFalAICostMetadata fallback floor', () => { ['fal-ai/nano-banana', FALAI_IMAGE_FALLBACK_PROVIDER_COST_DOLLARS], ['fal-ai/veo-3.1', FALAI_VIDEO_FALLBACK_PROVIDER_COST_DOLLARS], ['fal-ai/seedance-2.0', FALAI_VIDEO_FALLBACK_PROVIDER_COST_DOLLARS], + ['fal-ai/kling-video/v3/pro/image-to-video', FALAI_VIDEO_FALLBACK_PROVIDER_COST_DOLLARS], + ['blackforestlabs/flux-3/image-to-video', FALAI_VIDEO_FALLBACK_PROVIDER_COST_DOLLARS], ])('uses the correct provider-cost floor for %s', async (endpointId, expected) => { const result = await getFalAICostMetadata({ apiKey: 'test-key', diff --git a/apps/sim/lib/tools/falai-pricing.ts b/apps/sim/lib/tools/falai-pricing.ts index 3b3a9cb22da..2fdd51f1f2d 100644 --- a/apps/sim/lib/tools/falai-pricing.ts +++ b/apps/sim/lib/tools/falai-pricing.ts @@ -90,7 +90,9 @@ function getNumber(value: unknown): number | undefined { function getFalAIFallbackProviderCostDollars(endpointId: string): number { const normalizedEndpointId = endpointId.toLowerCase() - if (normalizedEndpointId.split('/').includes('text-to-video')) { + // Checked before the image match below, which every image-to-video endpoint id also contains. + const segments = normalizedEndpointId.split('/') + if (segments.includes('text-to-video') || segments.includes('image-to-video')) { return FALAI_VIDEO_FALLBACK_PROVIDER_COST_DOLLARS } diff --git a/apps/sim/providers/utils.test.ts b/apps/sim/providers/utils.test.ts index f3706baa456..051ebcb6457 100644 --- a/apps/sim/providers/utils.test.ts +++ b/apps/sim/providers/utils.test.ts @@ -2245,7 +2245,7 @@ describe('transformBlockTool configured selectors', () => { ) expect(result?.id).toBe('video_falai') - expect(result?.parameters?.properties).not.toHaveProperty('visualReference') + expect(result?.parameters?.properties).toHaveProperty('visualReference') const prepared = prepareToolExecution( result!, { prompt: 'A paper boat', provider: 'runway', model: 'gen-4-turbo' }, diff --git a/apps/sim/scripts/validate-fal-video-models.ts b/apps/sim/scripts/validate-fal-video-models.ts index cc73e67718b..f724e7a1c66 100644 --- a/apps/sim/scripts/validate-fal-video-models.ts +++ b/apps/sim/scripts/validate-fal-video-models.ts @@ -1,6 +1,6 @@ /** - * Checks every visible fal Video Generator option against its published queue schema, - * without submitting jobs. Run with bun apps/sim/scripts/validate-fal-video-models.ts. + * Checks every visible fal Video Generator option against its published queue schema, with and + * without a start frame (which targets the model's image-to-video endpoint), without submitting jobs. Run with bun apps/sim/scripts/validate-fal-video-models.ts. * FAL_VIDEO_SCHEMA_DIR optionally supplies saved schemas named .json. * FAL_VIDEO_REPORT_PATH optionally saves the per-model results. */ @@ -68,13 +68,24 @@ async function loadSchema(endpoint: string): Promise { return response.json() } -async function checkModel(id: string) { +const startFrame = { + id: 'schema-validation-frame', + name: 'frame.png', + url: '', + size: 1024, + type: 'image/png', + key: 'workspace/schema-validation/frame.png', +} + +async function checkModel(id: string, withImage: boolean) { const model = getFalVideoModel(id) assert(model) assert.equal(getFalVideoModel(model.endpoint), model) - const { paths, components } = await loadSchema(model.endpoint) + const endpoint = withImage ? model.image?.endpoint : model.endpoint + assert(endpoint) + const { paths, components } = await loadSchema(endpoint) const schemaAt = (ref?: string) => components.schemas[ref?.split('/').pop() ?? ''] - const path = `/${model.endpoint}` + const path = `/${endpoint}` const input = schemaAt(paths[path]?.post?.requestBody.content['application/json'].schema.$ref) const output = schemaAt( paths[`${path}/requests/{request_id}`]?.get?.responses['200'].content['application/json'].schema @@ -103,6 +114,7 @@ async function checkModel(id: string) { model: id, apiKey: 'schema-validation', prompt: 'A red kite rises above a meadow.', + ...(withImage ? { visualReference: startFrame } : {}), } const params = block.tools.config?.params if (!params) throw new Error('Video Generator must map block settings to tool parameters') @@ -110,7 +122,7 @@ async function checkModel(id: string) { duration: 'duration', resolution: 'resolution', aspectRatio: 'aspect_ratio', - generateAudio: model.audioField, + generateAudio: (withImage ? model.image : model)?.audioField, promptOptimizer: 'prompt_optimizer', } let count = 0 @@ -123,8 +135,13 @@ async function checkModel(id: string) { const parsed = videoToolBodySchema.parse(toolInput) const request = { ...parsed, provider: 'falai' } as const assert.equal(getFalAIValidationError(request), undefined) - const { body, endpoint } = buildFalVideoRequest(request) - assert.equal(endpoint, model?.endpoint) + const { body, endpoint: requestEndpoint, imageField } = buildFalVideoRequest(request) + assert.equal(requestEndpoint, endpoint) + assert.equal(imageField, withImage ? model?.image?.imageField : undefined) + if (withImage) { + assert.deepEqual(parsed.visualReference, startFrame) + if (imageField) body[imageField] = 'https://example.com/frame.png' + } assert(validate(body), JSON.stringify(validate.errors)) assert.equal(body.prompt, base.prompt) if (expected === 'default' && wireKey) assert(!Object.hasOwn(body, wireKey), wireKey) @@ -138,6 +155,12 @@ async function checkModel(id: string) { } count++ } + const startFrameField = block.subBlocks.find((field) => field.id === 'visualReferenceUpload') + assert.equal( + evaluateSubBlockCondition(startFrameField?.condition, base), + Boolean(model.image), + 'Start frame field should show exactly when the model has an image-to-video endpoint' + ) check({}) for (const field of block.subBlocks) { const key = field.id.replace(/^fal([A-Z])/, (_, initial: string) => initial.toLowerCase()) @@ -155,10 +178,14 @@ async function checkModel(id: string) { const results = [] for (const option of visible) { - try { - results.push({ model: option.id, passed: true, requests: await checkModel(option.id) }) - } catch (error) { - results.push({ model: option.id, passed: false, error: getErrorMessage(error) }) + const variants = getFalVideoModel(option.id)?.image ? [false, true] : [false] + for (const withImage of variants) { + const model = withImage ? `${option.id} (start frame)` : option.id + try { + results.push({ model, passed: true, requests: await checkModel(option.id, withImage) }) + } catch (error) { + results.push({ model, passed: false, error: getErrorMessage(error) }) + } } } if (process.env.FAL_VIDEO_REPORT_PATH) { diff --git a/apps/sim/serializer/field-analysis.test.ts b/apps/sim/serializer/field-analysis.test.ts index 78ec647d3f0..30ea8241171 100644 --- a/apps/sim/serializer/field-analysis.test.ts +++ b/apps/sim/serializer/field-analysis.test.ts @@ -16,6 +16,7 @@ vi.mock('@/blocks', () => ({ import { videoToolBodySchema } from '@/lib/api/contracts/tools/media/video' import { buildDirectVideoRequest } from '@/lib/internal/video/direct-request' +import { buildFalVideoRequest } from '@/lib/internal/video/fal-request' import { VideoGeneratorV3Block } from '@/blocks/blocks/video_generator' import { collectBlockFieldIssues, extractBlockParams } from '@/serializer/index' import { getToolMetadata, getToolParams } from '@/tools/metadata' @@ -208,6 +209,86 @@ describe('extractBlockParams', () => { expect(request.body).toMatchObject(payload) }) + it.each([ + { + name: 'a frame is attached after picking a prompt-only aspect ratio', + model: 'bytedance/seedance-2.5/text-to-video', + aspectRatio: '16:9', + frame: true, + endpoint: 'bytedance/seedance-2.5/image-to-video', + }, + { + name: 'the frame is removed after picking a frame-only aspect ratio', + model: 'fal-ai/bytedance/seedance/v1/pro/text-to-video', + aspectRatio: 'auto', + frame: false, + endpoint: 'fal-ai/bytedance/seedance/v1/pro/text-to-video', + }, + ])( + 'sends the model default for a fal setting the endpoint rejects when $name', + ({ model, aspectRatio, frame, endpoint }) => { + svcConfig.value = VideoGeneratorV3Block + const frameFile = { + id: 'f', + name: 'frame.png', + url: '', + size: 3, + type: 'image/png', + key: 'k', + } + const values = { + provider: 'falai', + apiKey: 'test-key', + model, + prompt: 'A cinematic sunrise', + falAspectRatio: aspectRatio, + ...(frame ? { visualReferenceInput: frameFile } : {}), + } + const params = extractBlockParams( + block({ + type: 'svc', + subBlocks: Object.fromEntries( + Object.entries(values).map(([id, value]) => [id, { id, type: 'short-input', value }]) + ), + }) + ) + const mapped = VideoGeneratorV3Block.tools.config?.params?.(params) + const request = buildFalVideoRequest({ + ...videoToolBodySchema.parse(mapped), + provider: 'falai', + }) + expect(request.endpoint).toBe(endpoint) + expect(request.body).not.toHaveProperty('aspect_ratio') + } + ) + + it('sends the model default for a Runway setting the start frame endpoint rejects', () => { + svcConfig.value = VideoGeneratorV3Block + const frameFile = { id: 'f', name: 'frame.png', url: '', size: 3, type: 'image/png', key: 'k' } + const values = { + provider: 'runway', + apiKey: 'test-key', + model: 'kling_4', + prompt: 'A cinematic sunrise', + aspectRatio: '1920:1080', + visualReferenceInput: frameFile, + } + const params = extractBlockParams( + block({ + type: 'svc', + subBlocks: Object.fromEntries( + Object.entries(values).map(([id, value]) => [id, { id, type: 'short-input', value }]) + ), + }) + ) + const mapped = VideoGeneratorV3Block.tools.config?.params?.(params) + const request = buildDirectVideoRequest({ + ...videoToolBodySchema.parse(mapped), + provider: 'runway', + }) + expect(request.body).not.toHaveProperty('ratio') + }) + it('resolves a canonical pair to its canonical id (advanced value wins when basic empty)', () => { svcConfig.value = config([ { diff --git a/apps/sim/tools/generated/tool-metadata.ts b/apps/sim/tools/generated/tool-metadata.ts index ab4930dd4f5..d68a11391c8 100644 --- a/apps/sim/tools/generated/tool-metadata.ts +++ b/apps/sim/tools/generated/tool-metadata.ts @@ -3,7 +3,7 @@ /** Serializable metadata for every built-in tool, keyed by tool id. */ const toolMetadata: Record = JSON.parse( - '{"a2a_cancel_task":{"id":"a2a_cancel_task","name":"A2A Cancel Task","description":"Request cancellation of an in-progress A2A task.","version":"1.0.0","params":{"agentUrl":{"type":"string","required":true,"visibility":"user-only","description":"The A2A agent endpoint URL"},"taskId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The task ID to cancel"},"apiKey":{"type":"string","required":false,"visibility":"user-only","description":"API key for authentication (if required)"}},"hostedApiKey":"none"},"a2a_get_agent_card":{"id":"a2a_get_agent_card","name":"A2A Get Agent Card","description":"Fetch the Agent Card (discovery document) for an external A2A agent.","version":"1.0.0","params":{"agentUrl":{"type":"string","required":true,"visibility":"user-only","description":"The A2A agent endpoint URL"},"apiKey":{"type":"string","required":false,"visibility":"user-only","description":"API key for authentication (if required)"}},"hostedApiKey":"none"},"a2a_get_task":{"id":"a2a_get_task","name":"A2A Get Task","description":"Retrieve the current state and result of an A2A task.","version":"1.0.0","params":{"agentUrl":{"type":"string","required":true,"visibility":"user-only","description":"The A2A agent endpoint URL"},"taskId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The task ID to retrieve"},"historyLength":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of history messages to include"},"apiKey":{"type":"string","required":false,"visibility":"user-only","description":"API key for authentication (if required)"}},"hostedApiKey":"none"},"a2a_send_message":{"id":"a2a_send_message","name":"A2A Send Message","description":"Send a message to an external A2A agent and return its response.","version":"1.0.0","params":{"agentUrl":{"type":"string","required":true,"visibility":"user-only","description":"The A2A agent endpoint URL"},"message":{"type":"string","required":true,"visibility":"user-or-llm","description":"The message text to send"},"data":{"type":"json","required":false,"visibility":"user-or-llm","description":"Optional structured JSON data to attach"},"files":{"type":"json","required":false,"visibility":"user-or-llm","description":"Optional files to attach"},"taskId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Existing task ID to continue"},"contextId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Conversation context ID to continue"},"apiKey":{"type":"string","required":false,"visibility":"user-only","description":"API key for authentication (if required)"}},"hostedApiKey":"none"},"affinity_batch_update_entity_fields":{"id":"affinity_batch_update_entity_fields","name":"Affinity Batch Update Entity Fields","description":"Write up to 100 non-list field values on one company or person in a single request.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which entity to write the fields on: companies or persons"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company or person"},"updates":{"type":"json","required":true,"visibility":"user-or-llm","description":"Up to 100 field updates as [{\\"id\\":\\"\\",\\"value\\":{\\"type\\":\\"…\\",\\"data\\":…}}], using the same value shapes as a single field update"}},"hostedApiKey":"none"},"affinity_batch_update_list_entry_fields":{"id":"affinity_batch_update_list_entry_fields","name":"Affinity Batch Update List Entry Fields","description":"Write up to 100 field values on one list row in a single request. Requires the \\"Export data from Lists\\" permission.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"listEntryId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list entry ID"},"updates":{"type":"json","required":true,"visibility":"user-or-llm","description":"Up to 100 field updates as [{\\"id\\":\\"\\",\\"value\\":{\\"type\\":\\"…\\",\\"data\\":…}}], using the same value shapes as a single field update"}},"hostedApiKey":"none"},"affinity_create_list":{"id":"affinity_create_list","name":"Affinity Create List","description":"Create a list. Its type fixes which entities it can hold, and the API key holder becomes its creator and owner.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the new list"},"type":{"type":"string","required":true,"visibility":"user-or-llm","description":"Entity kind the list holds: company, opportunity, or person"},"isPublic":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Whether everyone in the organization can see the list"}},"hostedApiKey":"none"},"affinity_create_list_field_dropdown_option":{"id":"affinity_create_list_field_dropdown_option","name":"Affinity Create List Field Dropdown Option","description":"Add a selectable option to a dropdown field on a list. A ranked or status option also needs a rank and a color.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown field ID on that list"},"type":{"type":"string","required":true,"visibility":"user-or-llm","description":"Kind of option to create, matching the field. dropdown takes only a label; ranked-dropdown also requires rank and color; status-dropdown additionally requires a status category. Sending a field the kind does not accept is rejected"},"text":{"type":"string","required":true,"visibility":"user-or-llm","description":"The option label"},"rank":{"type":"number","required":false,"visibility":"user-or-llm","description":"Sort order. Required on a ranked-dropdown or status-dropdown option"},"color":{"type":"string","required":false,"visibility":"user-or-llm","description":"Option color: white, gray, blue, green, purple, orange, or red. Required on a ranked-dropdown or status-dropdown option"},"statusCategory":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pipeline meaning of the option: open, won, lost, or on-hold. Status-dropdown options only"},"winRate":{"type":"number","required":false,"visibility":"user-or-llm","description":"Expected win rate of the status. Status-dropdown options only"}},"hostedApiKey":"none"},"affinity_create_merge":{"id":"affinity_create_merge","name":"Affinity Create Merge","description":"Fold a duplicate company or person into the record you are keeping. The merge runs asynchronously — poll the returned task to see it finish. Requires the \\"Manage duplicates\\" permission and an admin role.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"What to merge: companies or persons"},"primaryId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to keep"},"duplicateId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the duplicate record to fold in"}},"hostedApiKey":"none"},"affinity_create_note":{"id":"affinity_create_note","name":"Affinity Create Note","description":"Write a note — attached to companies, persons, and opportunities, anchored to a meeting, call, or chat message, or posted as a reply to an existing note.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"type":{"type":"string","required":true,"visibility":"user-or-llm","description":"Note shape: entities to attach it to records, interaction to anchor it to a meeting, call, or chat message, or user-reply to reply to a note"},"html":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note body as HTML"},"companyIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Companies to attach the note to, e.g. [1, 2]. Not used on a reply"},"personIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Persons to attach the note to, e.g. [1, 2]. Not used on a reply"},"opportunityIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Opportunities to attach the note to, e.g. [1, 2]. Not used on a reply"},"interactionId":{"type":"string","required":false,"visibility":"user-or-llm","description":"The interaction to anchor the note to. Required for an interaction note"},"interactionType":{"type":"string","required":false,"visibility":"user-or-llm","description":"Kind of the anchoring interaction: meeting, call, or chat-message. Required for an interaction note"},"parentId":{"type":"string","required":false,"visibility":"user-or-llm","description":"The note being replied to. Required for a user-reply note"},"creatorId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Attribute the note to another internal person. Defaults to the API key holder"},"createdAt":{"type":"string","required":false,"visibility":"user-or-llm","description":"Backdate the note to this ISO 8601 timestamp"}},"hostedApiKey":"none"},"affinity_create_reminder":{"id":"affinity_create_reminder","name":"Affinity Create Reminder","description":"Create a reminder on one company, person, or opportunity. A recurring reminder resets whenever the chosen signal happens instead of firing once.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"type":{"type":"string","required":true,"visibility":"user-or-llm","description":"one-time to fire once, or recurring to reset on a signal"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"What the reminder is about: company, person, or opportunity"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company, person, or opportunity"},"dueDate":{"type":"string","required":false,"visibility":"user-or-llm","description":"When the reminder is due, as an ISO 8601 timestamp. Required for a one-time reminder; on a recurring one Affinity computes it from the period when omitted"},"content":{"type":"string","required":false,"visibility":"user-or-llm","description":"What the reminder says"},"ownerId":{"type":"string","required":true,"visibility":"user-or-llm","description":"User the reminder is assigned to. Must be an internal user. The API key holder is recorded as the creator, which is a separate field"},"resetTrigger":{"type":"string","required":false,"visibility":"user-or-llm","description":"What restarts a recurring reminder: interaction, email, or event. Required when the type is recurring"},"periodDays":{"type":"number","required":false,"visibility":"user-or-llm","description":"Days between firings of a recurring reminder. Required when the type is recurring"}},"hostedApiKey":"none"},"affinity_delete_list_field_dropdown_option":{"id":"affinity_delete_list_field_dropdown_option","name":"Affinity Delete List Field Dropdown Option","description":"Permanently delete a dropdown option on a list field. Every list entry currently set to it is cleared, and those values cannot be recovered.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown field ID on that list"},"dropdownOptionId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown option ID to delete"}},"hostedApiKey":"none"},"affinity_delete_note":{"id":"affinity_delete_note","name":"Affinity Delete Note","description":"Delete a note you created. Deleting a root note also deletes its replies; deleting a reply removes only that reply.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"noteId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note ID to delete"}},"hostedApiKey":"none"},"affinity_get_company":{"id":"affinity_get_company","name":"Affinity Get Company","description":"Look up one company by ID. Field data is returned only for the Field IDs or Field Types asked for.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"companyId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The company ID"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, or relationship-intelligence. Mutually exclusive with Field IDs"}},"hostedApiKey":"none"},"affinity_get_current_user":{"id":"affinity_get_current_user","name":"Affinity Get Current User","description":"Verify an Affinity API key and return the tenant, the user behind the key, and the scopes the grant carries.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"}},"hostedApiKey":"none"},"affinity_get_entity_field_value":{"id":"affinity_get_entity_field_value","name":"Affinity Get Entity Field Value","description":"Read one non-list field value from a company or person.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which entity to read the field from: companies or persons"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company or person"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The field ID to read"}},"hostedApiKey":"none"},"affinity_get_list":{"id":"affinity_get_list","name":"Affinity Get List","description":"Read one list — its name, type, owner, and privacy setting.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"}},"hostedApiKey":"none"},"affinity_get_list_entry":{"id":"affinity_get_list_entry","name":"Affinity Get List Entry","description":"Read one row of a list with its entity. Field data is returned only for the Field IDs or Field Types asked for.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"listEntryId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list entry ID"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, list, or relationship-intelligence. Mutually exclusive with Field IDs"}},"hostedApiKey":"none"},"affinity_get_list_entry_field":{"id":"affinity_get_list_entry_field","name":"Affinity Get List Entry Field","description":"Read one field value on a list row.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"listEntryId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list entry ID"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The field ID to read"}},"hostedApiKey":"none"},"affinity_get_list_field_dropdown_option":{"id":"affinity_get_list_field_dropdown_option","name":"Affinity Get List Field Dropdown Option","description":"Read one dropdown option on a list field.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown field ID on that list"},"dropdownOptionId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown option ID"}},"hostedApiKey":"none"},"affinity_get_merge":{"id":"affinity_get_merge","name":"Affinity Get Merge","description":"Read the status of one company or person merge, including why it failed if it did.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which merge to read: companies or persons"},"mergeId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The merge ID"}},"hostedApiKey":"none"},"affinity_get_merge_task":{"id":"affinity_get_merge_task","name":"Affinity Get Merge Task","description":"Read one merge task and how its merges are progressing. Poll this after starting a merge.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which merge task to read: companies or persons"},"taskId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The merge task ID"}},"hostedApiKey":"none"},"affinity_get_note":{"id":"affinity_get_note","name":"Affinity Get Note","description":"Read one note with its body, author, mentions, and attached records.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"noteId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note ID"},"includes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Extra properties to return, e.g. [\\"repliesCount\\",\\"personsPreview\\",\\"companiesPreview\\",\\"opportunitiesPreview\\"]. Those four fields are omitted unless requested here"}},"hostedApiKey":"none"},"affinity_get_opportunity":{"id":"affinity_get_opportunity","name":"Affinity Get Opportunity","description":"Read one opportunity and the list it belongs to. Its field data lives on the list entry.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"opportunityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The opportunity ID"}},"hostedApiKey":"none"},"affinity_get_person":{"id":"affinity_get_person","name":"Affinity Get Person","description":"Look up one person by ID. Field data is returned only for the Field IDs or Field Types asked for.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"personId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The person ID"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, or relationship-intelligence. Mutually exclusive with Field IDs"}},"hostedApiKey":"none"},"affinity_get_saved_view":{"id":"affinity_get_saved_view","name":"Affinity Get Saved View","description":"Read one saved view — its name, kind, and creation date.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"viewId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The saved view ID"}},"hostedApiKey":"none"},"affinity_get_transcript":{"id":"affinity_get_transcript","name":"Affinity Get Transcript","description":"Read one transcript with its first 100 fragments. Page the fragments endpoint for a longer meeting.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"transcriptId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The transcript ID"}},"hostedApiKey":"none"},"affinity_get_user":{"id":"affinity_get_user","name":"Affinity Get User","description":"Read one internal user. A user and their person record share the same numeric ID, so a person ID works here.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"userId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The user ID, which is also their person ID"}},"hostedApiKey":"none"},"affinity_list_calls":{"id":"affinity_list_calls","name":"Affinity List Calls","description":"Page through logged calls and their participants. Only calls the API key holder can see are returned.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_chat_messages":{"id":"affinity_list_chat_messages","name":"Affinity List Chat Messages","description":"Page through logged chat messages and their participants. Only messages the API key holder can see are returned.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_companies":{"id":"affinity_list_companies","name":"Affinity List Companies","description":"Page through companies. Companies come back without field data unless Field IDs or Field Types asks for it.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"ids":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict the page to these company IDs, e.g. [1, 2, 3]"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, or relationship-intelligence. Mutually exclusive with Field IDs"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_coworker_connections":{"id":"affinity_list_coworker_connections","name":"Affinity List Coworker Connections","description":"Find warm paths into a company through shared work history: who in your Affinity data once worked alongside the people you want to reach. Grouped by target, strongest first.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":true,"visibility":"user-or-llm","description":"Required scope. The only supported filter is target.currentCompany.id, e.g. \\"target.currentCompany.id=123\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of targets to return per page, 1-50. Defaults to 20"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_emails":{"id":"affinity_list_emails","name":"Affinity List Emails","description":"Page through email metadata — subject, participants, and timestamps. Affinity never exposes email bodies through the API.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_entity_field_values":{"id":"affinity_list_entity_field_values","name":"Affinity List Entity Field Values","description":"Page through a company\'s or person\'s non-list field values. List fields are not returned here — read those through the list entry.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which entity to read field values from: companies or persons"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company or person"},"ids":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict to these field IDs. Mutually exclusive with Field Types"},"types":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict to these field categories: enriched, global, relationship-intelligence. Mutually exclusive with Field IDs"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 20"}},"hostedApiKey":"none"},"affinity_list_entity_list_entries":{"id":"affinity_list_entity_list_entries","name":"Affinity List Entity List Entries","description":"Page through a company\'s or person\'s rows across every list, each carrying that list\'s field values and when the entity was added.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which entity to look up the rows of: companies or persons"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company or person"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_entity_lists":{"id":"affinity_list_entity_lists","name":"Affinity List Entity Lists","description":"List every list a company or person appears on that the caller can view.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which entity to look up the lists of: companies or persons"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company or person"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_entity_notes":{"id":"affinity_list_entity_notes","name":"Affinity List Entity Notes","description":"List the notes relevant to one company, person, or opportunity — directly attached notes plus notes reaching it through its people and meetings.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which entity the notes hang off: companies, persons, or opportunities"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company, person, or opportunity"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_entity_relationships":{"id":"affinity_list_entity_relationships","name":"Affinity List Entity Relationships","description":"List who knows a company or person, scored 0.0 to 1.0 by how much the two actually interact. Strongest first by default.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which entity to look up relationships for: companies or persons"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company or person"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression. This endpoint filters on interactionScore only, e.g. \\"interactionScore>=0.5\\""},"orderBy":{"type":"json","required":false,"visibility":"user-or-llm","description":"Sort order: [\\"interactionScore\\"] for weakest first, [\\"-interactionScore\\"] for strongest first (the default)"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_field_dropdown_options":{"id":"affinity_list_field_dropdown_options","name":"Affinity List Field Dropdown Options","description":"List the selectable options on a dropdown or ranked-dropdown company or person field. Writing such a field needs the option ID, not its text.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which field family the field belongs to: companies or persons"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown or ranked-dropdown field ID"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_field_metadata":{"id":"affinity_list_field_metadata","name":"Affinity List Field Metadata","description":"List the non-list company or person fields, with the value type, filter operators, and sort support of each. Start here to find the Field IDs the read and write tools take.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which fields to describe: companies or persons"},"includes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Extra properties to return: [\\"filterability\\",\\"sortability\\"]. Both are omitted unless requested here"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression. This endpoint filters on name only, e.g. \\"name=~Status\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_field_value_changes":{"id":"affinity_list_field_value_changes","name":"Affinity List Field Value Changes","description":"Page through field value changes across the whole workspace. Built for delta sync: follow nextCursor to the end of a run, then resume from the last cursor next time.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression over field.id, listEntry.id, changer.id, changedAt, or actionType. Resume a sync with e.g. \\"changedAt>2026-06-01T12:00:00Z\\""},"orderBy":{"type":"json","required":false,"visibility":"user-or-llm","description":"Sort order: [\\"changedAt\\"] for oldest first (the default), [\\"-changedAt\\"] for newest first"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_investor_executive_connections":{"id":"affinity_list_investor_executive_connections","name":"Affinity List Investor Executive Connections","description":"Find warm paths into a company through investment history: which investors in your Affinity data backed a company the people you want to reach once led. Grouped by target, strongest first.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":true,"visibility":"user-or-llm","description":"Required scope. The only supported filter is target.currentCompany.id, e.g. \\"target.currentCompany.id=123\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of targets to return per page, 1-50. Defaults to 20"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_list_entries":{"id":"affinity_list_list_entries","name":"Affinity List List Entries","description":"Page through the rows of a list. Rows come back without field data unless Field IDs or Field Types asks for it.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, list, or relationship-intelligence. Mutually exclusive with Field IDs"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_list_entry_field_value_changes":{"id":"affinity_list_list_entry_field_value_changes","name":"Affinity List List Entry Field Value Changes","description":"Page through the history of one list row — who changed which field, when, and to what.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"listEntryId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list entry ID"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression over field.id, changer.id, changedAt, or actionType, e.g. \\"field.id=field-1234\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_list_entry_fields":{"id":"affinity_list_list_entry_fields","name":"Affinity List List Entry Fields","description":"Page through every field value on one list row, including the list-specific columns. All fields are returned unless narrowed.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"listEntryId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list entry ID"},"ids":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict to these field IDs. Mutually exclusive with Field Types"},"types":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict to these field categories: enriched, global, list, relationship-intelligence. Mutually exclusive with Field IDs"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 20"}},"hostedApiKey":"none"},"affinity_list_list_field_dropdown_options":{"id":"affinity_list_list_field_dropdown_options","name":"Affinity List List Field Dropdown Options","description":"List the selectable options on a dropdown, ranked-dropdown, or status-dropdown field of a list.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown field ID on that list"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_list_fields":{"id":"affinity_list_list_fields","name":"Affinity List List Fields","description":"List the fields available on one list, including its list-specific columns. Use these Field IDs when reading or writing list entries.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"includes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Extra properties to return: [\\"filterability\\",\\"sortability\\"]. Both are omitted unless requested here"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression. This endpoint filters on name only, e.g. \\"name=~Stage\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_lists":{"id":"affinity_list_lists","name":"Affinity List Lists","description":"Page through the lists in the organization that the caller can view.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"term":{"type":"string","required":false,"visibility":"user-or-llm","description":"Case-insensitive substring match on the list name"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_meetings":{"id":"affinity_list_meetings","name":"Affinity List Meetings","description":"Page through past and upcoming meetings with their organizer and attendees.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_merge_tasks":{"id":"affinity_list_merge_tasks","name":"Affinity List Merge Tasks","description":"Page through merge tasks, each summarizing how many of its merges are in progress, succeeded, or failed.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which merge tasks to list: companies or persons"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression. This endpoint filters on status only, e.g. \\"status=in-progress\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_merges":{"id":"affinity_list_merges","name":"Affinity List Merges","description":"Page through the company or person merges the organization has run, with the status and the records involved in each.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which merges to list: companies or persons"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression over status or taskId, e.g. \\"status=failed\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_note_attached_companies":{"id":"affinity_list_note_attached_companies","name":"Affinity List Note Attached Companies","description":"List the companies directly attached to one note.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"noteId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note ID"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_note_attached_opportunities":{"id":"affinity_list_note_attached_opportunities","name":"Affinity List Note Attached Opportunities","description":"List the opportunities directly attached to one note.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"noteId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note ID"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_note_attached_persons":{"id":"affinity_list_note_attached_persons","name":"Affinity List Note Attached Persons","description":"List the persons directly attached to one note.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"noteId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note ID"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_note_replies":{"id":"affinity_list_note_replies","name":"Affinity List Note Replies","description":"Page through the replies on one note, including AI Notetaker replies.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"noteId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note ID whose replies to read"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_notes":{"id":"affinity_list_notes","name":"Affinity List Notes","description":"Page through every note the caller can see. Replies are excluded.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"includes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Extra properties to return, e.g. [\\"repliesCount\\",\\"personsPreview\\",\\"companiesPreview\\",\\"opportunitiesPreview\\"]. Those four fields are omitted unless requested here"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_opportunities":{"id":"affinity_list_opportunities","name":"Affinity List Opportunities","description":"Page through opportunities. Field data lives on the list entry, not here — read it through the list or saved view the opportunity belongs to.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"ids":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict the page to these opportunity IDs, e.g. [1, 2, 3]"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_persons":{"id":"affinity_list_persons","name":"Affinity List Persons","description":"Page through persons. Persons come back without field data unless Field IDs or Field Types asks for it.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"ids":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict the page to these person IDs, e.g. [1, 2, 3]"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, or relationship-intelligence. Mutually exclusive with Field IDs"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_reminders":{"id":"affinity_list_reminders","name":"Affinity List Reminders","description":"Page through the reminders the caller can see. Filter by status to surface what is overdue.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_saved_view_entries":{"id":"affinity_list_saved_view_entries","name":"Affinity List Saved View Entries","description":"Page through the rows of a saved view. The view\'s own filters and columns decide which rows and which field data come back.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"viewId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The saved view ID"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_saved_views":{"id":"affinity_list_saved_views","name":"Affinity List Saved Views","description":"List the saved views on a list that the caller can view.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_transcript_fragments":{"id":"affinity_list_transcript_fragments","name":"Affinity List Transcript Fragments","description":"Page through everything said in a meeting, segment by segment with the speaker.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"transcriptId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The transcript ID"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_transcripts":{"id":"affinity_list_transcripts","name":"Affinity List Transcripts","description":"Page through meeting transcript metadata. Read one transcript to get what was actually said.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_users":{"id":"affinity_list_users","name":"Affinity List Users","description":"Page through the internal users in the organization. Email addresses and roles are returned only to callers with the \\"Manage Users\\" permission.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"term":{"type":"string","required":false,"visibility":"user-or-llm","description":"Case-insensitive match across first name, last name, and primary email"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression over id or status, e.g. \\"status=active\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_search_companies":{"id":"affinity_search_companies","name":"Affinity Search Companies","description":"Search companies by filters, sorts, and a free-text term. Requires the \\"Export All Organizations directory\\" permission.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filters":{"type":"json","required":false,"visibility":"user-or-llm","description":"Filter group as {operator: \\"and\\"|\\"or\\", filters: [...]}, at most 50 leaves. Each leaf is {valueType, fieldId, operator, value}, and a leaf may itself be a nested group"},"searchTerm":{"type":"string","required":false,"visibility":"user-or-llm","description":"Free-text term matched against the searchable fields. At least 3 characters"},"searchFieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs the search term is matched against. Defaults to the searchable fields"},"sorts":{"type":"json","required":false,"visibility":"user-or-llm","description":"Sort order as [{fieldId, direction: \\"asc\\"|\\"desc\\", attributeId?}], up to 5, applied in order"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, or relationship-intelligence. Mutually exclusive with Field IDs"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_search_files":{"id":"affinity_search_files","name":"Affinity Search Files","description":"Search files by keyword, ordered by relevance. Narrow to specific files or to one company, or leave both unset to search the whole account.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"prompt":{"type":"string","required":true,"visibility":"user-or-llm","description":"What to search for. Between 3 and 500 characters"},"ids":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict the search to these file IDs. Cannot be combined with Company ID"},"companyId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Restrict the search to one company\'s files. Cannot be combined with file IDs"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of files to return, 1-100. Defaults to 20"}},"hostedApiKey":"none"},"affinity_search_list_entries":{"id":"affinity_search_list_entries","name":"Affinity Search List Entries","description":"Search the rows of one list by filters, sorts, and a free-text term. Requires the \\"Export data from Lists\\" permission.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID to search"},"filters":{"type":"json","required":false,"visibility":"user-or-llm","description":"Filter group as {operator: \\"and\\"|\\"or\\", filters: [...]}, at most 50 leaves. Each leaf is {valueType, fieldId, operator, value}, and a leaf may itself be a nested group"},"searchTerm":{"type":"string","required":false,"visibility":"user-or-llm","description":"Free-text term matched against the searchable fields. At least 3 characters"},"searchFieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs the search term is matched against. Defaults to the searchable fields"},"sorts":{"type":"json","required":false,"visibility":"user-or-llm","description":"Sort order as [{fieldId, direction: \\"asc\\"|\\"desc\\", attributeId?}], up to 5, applied in order"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, list, or relationship-intelligence. Mutually exclusive with Field IDs"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_search_notes":{"id":"affinity_search_notes","name":"Affinity Search Notes","description":"Search notes by keyword, ordered by relevance. Narrow to specific notes or to one company, or leave both unset to search the whole account.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"prompt":{"type":"string","required":true,"visibility":"user-or-llm","description":"What to search for. Between 3 and 500 characters"},"ids":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict the search to these note IDs. Cannot be combined with Company ID"},"companyId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Restrict the search to one company\'s notes. Cannot be combined with note IDs"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of notes to return, 1-100. Defaults to 20"}},"hostedApiKey":"none"},"affinity_search_persons":{"id":"affinity_search_persons","name":"Affinity Search Persons","description":"Search persons by filters, sorts, and a free-text term. Requires the \\"Export All People directory\\" permission.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filters":{"type":"json","required":false,"visibility":"user-or-llm","description":"Filter group as {operator: \\"and\\"|\\"or\\", filters: [...]}, at most 50 leaves. Each leaf is {valueType, fieldId, operator, value}, and a leaf may itself be a nested group"},"searchTerm":{"type":"string","required":false,"visibility":"user-or-llm","description":"Free-text term matched against the searchable fields. At least 3 characters"},"searchFieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs the search term is matched against. Defaults to the searchable fields"},"sorts":{"type":"json","required":false,"visibility":"user-or-llm","description":"Sort order as [{fieldId, direction: \\"asc\\"|\\"desc\\", attributeId?}], up to 5, applied in order"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, or relationship-intelligence. Mutually exclusive with Field IDs"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_semantic_search":{"id":"affinity_semantic_search","name":"Affinity Semantic Search","description":"Find companies from a description in plain language — industry, technology, stage, or business model. Currently searches companies only.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"prompt":{"type":"string","required":true,"visibility":"user-or-llm","description":"What to look for, in plain language, e.g. \\"climate tech companies in our pipeline\\". Up to 500 characters"},"listIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict the search to companies on these lists, e.g. [1, 2]"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of companies to return, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_update_entity_field_value":{"id":"affinity_update_entity_field_value","name":"Affinity Update Entity Field Value","description":"Write one non-list field value on a company or person. The value type must match how the field is defined.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which entity to write the field on: companies or persons"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company or person"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The field ID to write"},"value":{"type":"json","required":true,"visibility":"user-or-llm","description":"The new value as {type, data}, where type matches the field\'s value type. Examples: {\\"type\\":\\"text\\",\\"data\\":\\"Series B\\"}, {\\"type\\":\\"number\\",\\"data\\":42}, {\\"type\\":\\"dropdown\\",\\"data\\":{\\"dropdownOptionId\\":7}}, {\\"type\\":\\"person\\",\\"data\\":{\\"id\\":123}}, {\\"type\\":\\"person-multi\\",\\"data\\":[{\\"id\\":123}]}. Pass data as null to clear the field"}},"hostedApiKey":"none"},"affinity_update_list_entry_field":{"id":"affinity_update_list_entry_field","name":"Affinity Update List Entry Field","description":"Write one field value on a list row. Requires the \\"Export data from Lists\\" permission.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"listEntryId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list entry ID"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The field ID to write"},"value":{"type":"json","required":true,"visibility":"user-or-llm","description":"The new value as {type, data}, where type matches the field\'s value type. Examples: {\\"type\\":\\"text\\",\\"data\\":\\"Series B\\"}, {\\"type\\":\\"number\\",\\"data\\":42}, {\\"type\\":\\"dropdown\\",\\"data\\":{\\"dropdownOptionId\\":7}}, {\\"type\\":\\"person\\",\\"data\\":{\\"id\\":123}}, {\\"type\\":\\"person-multi\\",\\"data\\":[{\\"id\\":123}]}. Pass data as null to clear the field"}},"hostedApiKey":"none"},"affinity_update_list_field_dropdown_option":{"id":"affinity_update_list_field_dropdown_option","name":"Affinity Update List Field Dropdown Option","description":"Change a dropdown option on a list field. Every field is optional — supply only what should change, and only fields the option\'s kind actually has.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown field ID on that list"},"dropdownOptionId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown option ID to update"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Replacement option label. Supply at least one field to change"},"rank":{"type":"number","required":false,"visibility":"user-or-llm","description":"Sort order. Required on a ranked-dropdown or status-dropdown option"},"color":{"type":"string","required":false,"visibility":"user-or-llm","description":"Option color: white, gray, blue, green, purple, orange, or red. Required on a ranked-dropdown or status-dropdown option"},"statusCategory":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pipeline meaning of the option: open, won, lost, or on-hold. Status-dropdown options only"},"winRate":{"type":"number","required":false,"visibility":"user-or-llm","description":"Expected win rate of the status. Status-dropdown options only"}},"hostedApiKey":"none"},"affinity_update_note":{"id":"affinity_update_note","name":"Affinity Update Note","description":"Rewrite a note\'s body or replace which records it is attached to. Each list of IDs replaces that association wholesale, an empty list clears it, and omitting one leaves it untouched. A note\'s type cannot be changed.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"noteId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note ID to update"},"html":{"type":"string","required":false,"visibility":"user-or-llm","description":"Replacement note body as HTML"},"companyIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Replacement set of attached companies, e.g. [1, 2]. Send [] to detach every company; omit to leave them unchanged"},"personIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Replacement set of attached persons, e.g. [1, 2]. Send [] to detach every person; omit to leave them unchanged"},"opportunityIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Replacement set of attached opportunities, e.g. [1, 2]. Send [] to detach every opportunity; omit to leave them unchanged"}},"hostedApiKey":"none"},"agentmail_create_draft":{"id":"agentmail_create_draft","name":"Create Draft","description":"Create a new email draft in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to create the draft in"},"to":{"type":"string","required":false,"visibility":"user-or-llm","description":"Recipient email addresses (comma-separated)"},"subject":{"type":"string","required":false,"visibility":"user-or-llm","description":"Draft subject line"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Plain text draft body"},"html":{"type":"string","required":false,"visibility":"user-or-llm","description":"HTML draft body"},"cc":{"type":"string","required":false,"visibility":"user-or-llm","description":"CC recipient email addresses (comma-separated)"},"bcc":{"type":"string","required":false,"visibility":"user-or-llm","description":"BCC recipient email addresses (comma-separated)"},"inReplyTo":{"type":"string","required":false,"visibility":"user-or-llm","description":"ID of message being replied to"},"sendAt":{"type":"string","required":false,"visibility":"user-or-llm","description":"ISO 8601 timestamp to schedule sending"}},"hostedApiKey":"none"},"agentmail_create_inbox":{"id":"agentmail_create_inbox","name":"Create Inbox","description":"Create a new email inbox with AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"username":{"type":"string","required":false,"visibility":"user-or-llm","description":"Username for the inbox email address"},"domain":{"type":"string","required":false,"visibility":"user-or-llm","description":"Domain for the inbox email address"},"displayName":{"type":"string","required":false,"visibility":"user-or-llm","description":"Display name for the inbox"}},"hostedApiKey":"none"},"agentmail_delete_draft":{"id":"agentmail_delete_draft","name":"Delete Draft","description":"Delete an email draft in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the draft"},"draftId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the draft to delete"}},"hostedApiKey":"none"},"agentmail_delete_inbox":{"id":"agentmail_delete_inbox","name":"Delete Inbox","description":"Delete an email inbox in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to delete"}},"hostedApiKey":"none"},"agentmail_delete_thread":{"id":"agentmail_delete_thread","name":"Delete Thread","description":"Delete an email thread in AgentMail (moves to trash, or permanently deletes if already in trash)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the thread"},"threadId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the thread to delete"},"permanent":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Force permanent deletion instead of moving to trash"}},"hostedApiKey":"none"},"agentmail_forward_message":{"id":"agentmail_forward_message","name":"Forward Message","description":"Forward an email message to new recipients in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the message"},"messageId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the message to forward"},"to":{"type":"string","required":true,"visibility":"user-or-llm","description":"Recipient email addresses (comma-separated)"},"subject":{"type":"string","required":false,"visibility":"user-or-llm","description":"Override subject line"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Additional plain text to prepend"},"html":{"type":"string","required":false,"visibility":"user-or-llm","description":"Additional HTML to prepend"},"cc":{"type":"string","required":false,"visibility":"user-or-llm","description":"CC recipient email addresses (comma-separated)"},"bcc":{"type":"string","required":false,"visibility":"user-or-llm","description":"BCC recipient email addresses (comma-separated)"}},"hostedApiKey":"none"},"agentmail_get_draft":{"id":"agentmail_get_draft","name":"Get Draft","description":"Get details of a specific email draft in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox the draft belongs to"},"draftId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the draft to retrieve"}},"hostedApiKey":"none"},"agentmail_get_inbox":{"id":"agentmail_get_inbox","name":"Get Inbox","description":"Get details of a specific email inbox in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to retrieve"}},"hostedApiKey":"none"},"agentmail_get_message":{"id":"agentmail_get_message","name":"Get Message","description":"Get details of a specific email message in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the message"},"messageId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the message to retrieve"}},"hostedApiKey":"none"},"agentmail_get_thread":{"id":"agentmail_get_thread","name":"Get Thread","description":"Get details of a specific email thread including messages in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the thread"},"threadId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the thread to retrieve"}},"hostedApiKey":"none"},"agentmail_list_drafts":{"id":"agentmail_list_drafts","name":"List Drafts","description":"List email drafts in an inbox in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to list drafts from"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of drafts to return"},"pageToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token for next page of results"}},"hostedApiKey":"none"},"agentmail_list_inboxes":{"id":"agentmail_list_inboxes","name":"List Inboxes","description":"List all email inboxes in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of inboxes to return"},"pageToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token for next page of results"}},"hostedApiKey":"none"},"agentmail_list_messages":{"id":"agentmail_list_messages","name":"List Messages","description":"List messages in an inbox in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to list messages from"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of messages to return"},"pageToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token for next page of results"}},"hostedApiKey":"none"},"agentmail_list_threads":{"id":"agentmail_list_threads","name":"List Threads","description":"List email threads in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to list threads from"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of threads to return"},"pageToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token for next page of results"},"labels":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated labels to filter threads by"},"before":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter threads before this ISO 8601 timestamp"},"after":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter threads after this ISO 8601 timestamp"}},"hostedApiKey":"none"},"agentmail_reply_message":{"id":"agentmail_reply_message","name":"Reply to Message","description":"Reply to an existing email message in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to reply from"},"messageId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the message to reply to"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Plain text reply body"},"html":{"type":"string","required":false,"visibility":"user-or-llm","description":"HTML reply body"},"to":{"type":"string","required":false,"visibility":"user-or-llm","description":"Override recipient email addresses (comma-separated)"},"cc":{"type":"string","required":false,"visibility":"user-or-llm","description":"CC email addresses (comma-separated)"},"bcc":{"type":"string","required":false,"visibility":"user-or-llm","description":"BCC email addresses (comma-separated)"},"replyAll":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Reply to all recipients of the original message"}},"hostedApiKey":"none"},"agentmail_send_draft":{"id":"agentmail_send_draft","name":"Send Draft","description":"Send an existing email draft in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the draft"},"draftId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the draft to send"}},"hostedApiKey":"none"},"agentmail_send_message":{"id":"agentmail_send_message","name":"Send Message","description":"Send an email message from an AgentMail inbox","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to send from"},"to":{"type":"string","required":true,"visibility":"user-or-llm","description":"Recipient email address (comma-separated for multiple)"},"subject":{"type":"string","required":true,"visibility":"user-or-llm","description":"Email subject line"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Plain text email body"},"html":{"type":"string","required":false,"visibility":"user-or-llm","description":"HTML email body"},"cc":{"type":"string","required":false,"visibility":"user-or-llm","description":"CC recipient email addresses (comma-separated)"},"bcc":{"type":"string","required":false,"visibility":"user-or-llm","description":"BCC recipient email addresses (comma-separated)"}},"hostedApiKey":"none"},"agentmail_update_draft":{"id":"agentmail_update_draft","name":"Update Draft","description":"Update an existing email draft in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the draft"},"draftId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the draft to update"},"to":{"type":"string","required":false,"visibility":"user-or-llm","description":"Recipient email addresses (comma-separated)"},"subject":{"type":"string","required":false,"visibility":"user-or-llm","description":"Draft subject line"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Plain text draft body"},"html":{"type":"string","required":false,"visibility":"user-or-llm","description":"HTML draft body"},"cc":{"type":"string","required":false,"visibility":"user-or-llm","description":"CC recipient email addresses (comma-separated)"},"bcc":{"type":"string","required":false,"visibility":"user-or-llm","description":"BCC recipient email addresses (comma-separated)"},"sendAt":{"type":"string","required":false,"visibility":"user-or-llm","description":"ISO 8601 timestamp to schedule sending"}},"hostedApiKey":"none"},"agentmail_update_inbox":{"id":"agentmail_update_inbox","name":"Update Inbox","description":"Update the display name of an email inbox in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to update"},"displayName":{"type":"string","required":true,"visibility":"user-or-llm","description":"New display name for the inbox"}},"hostedApiKey":"none"},"agentmail_update_message":{"id":"agentmail_update_message","name":"Update Message","description":"Add or remove labels on an email message in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the message"},"messageId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the message to update"},"addLabels":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated labels to add to the message"},"removeLabels":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated labels to remove from the message"}},"hostedApiKey":"none"},"agentmail_update_thread":{"id":"agentmail_update_thread","name":"Update Thread Labels","description":"Add or remove labels on an email thread in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the thread"},"threadId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the thread to update"},"addLabels":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated labels to add to the thread"},"removeLabels":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated labels to remove from the thread"}},"hostedApiKey":"none"},"agentphone_create_call":{"id":"agentphone_create_call","name":"Create Outbound Call","description":"Initiate an outbound voice call from an AgentPhone agent","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"agentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Agent that will handle the call"},"toNumber":{"type":"string","required":true,"visibility":"user-or-llm","description":"Phone number to call in E.164 format (e.g. +14155551234)"},"fromNumberId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Phone number ID to use as caller ID. Must belong to the agent. If omitted, the agent\'s first assigned number is used."},"initialGreeting":{"type":"string","required":false,"visibility":"user-or-llm","description":"Optional greeting spoken when the recipient answers"},"voice":{"type":"string","required":false,"visibility":"user-or-llm","description":"Voice ID override for this call (defaults to the agent\'s configured voice)"},"systemPrompt":{"type":"string","required":false,"visibility":"user-or-llm","description":"When provided, uses a built-in LLM for the conversation instead of forwarding to your webhook"}},"hostedApiKey":"none"},"agentphone_create_contact":{"id":"agentphone_create_contact","name":"Create Contact","description":"Create a new contact in AgentPhone","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"phoneNumber":{"type":"string","required":true,"visibility":"user-or-llm","description":"Phone number in E.164 format (e.g. +14155551234)"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Contact\'s full name"},"email":{"type":"string","required":false,"visibility":"user-or-llm","description":"Contact\'s email address"},"notes":{"type":"string","required":false,"visibility":"user-or-llm","description":"Freeform notes stored on the contact"}},"hostedApiKey":"none"},"agentphone_create_number":{"id":"agentphone_create_number","name":"Create Phone Number","description":"Provision a new SMS- and voice-enabled phone number","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Two-letter country code (e.g. US, CA). Defaults to US."},"areaCode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Preferred area code (US/CA only, e.g. \\"415\\"). Best-effort — may be ignored if unavailable."},"agentId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Optionally attach the number to an agent immediately"}},"hostedApiKey":"none"},"agentphone_delete_contact":{"id":"agentphone_delete_contact","name":"Delete Contact","description":"Delete a contact by ID","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"contactId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Contact ID"}},"hostedApiKey":"none"},"agentphone_get_call":{"id":"agentphone_get_call","name":"Get Call","description":"Fetch a call and its full transcript","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"callId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the call to retrieve"}},"hostedApiKey":"none"},"agentphone_get_call_transcript":{"id":"agentphone_get_call_transcript","name":"Get Call Transcript","description":"Get the full ordered transcript for a call","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"callId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the call to retrieve the transcript for"}},"hostedApiKey":"none"},"agentphone_get_contact":{"id":"agentphone_get_contact","name":"Get Contact","description":"Fetch a single contact by ID","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"contactId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Contact ID"}},"hostedApiKey":"none"},"agentphone_get_conversation":{"id":"agentphone_get_conversation","name":"Get Conversation","description":"Get a conversation along with its recent messages","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"conversationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Conversation ID"},"messageLimit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of recent messages to include (default 50, max 100)"}},"hostedApiKey":"none"},"agentphone_get_conversation_messages":{"id":"agentphone_get_conversation_messages","name":"Get Conversation Messages","description":"Get paginated messages for a conversation","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"conversationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Conversation ID"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of messages to return (default 50, max 200)"},"before":{"type":"string","required":false,"visibility":"user-or-llm","description":"Return messages received before this ISO 8601 timestamp"},"after":{"type":"string","required":false,"visibility":"user-or-llm","description":"Return messages received after this ISO 8601 timestamp"}},"hostedApiKey":"none"},"agentphone_get_number_messages":{"id":"agentphone_get_number_messages","name":"Get Phone Number Messages","description":"Fetch messages received on a specific phone number","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"numberId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the phone number"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of messages to return (default 50, max 200)"},"before":{"type":"string","required":false,"visibility":"user-or-llm","description":"Return messages received before this ISO 8601 timestamp"},"after":{"type":"string","required":false,"visibility":"user-or-llm","description":"Return messages received after this ISO 8601 timestamp"}},"hostedApiKey":"none"},"agentphone_get_usage":{"id":"agentphone_get_usage","name":"Get Usage","description":"Retrieve current usage statistics for the AgentPhone account","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"}},"hostedApiKey":"none"},"agentphone_get_usage_daily":{"id":"agentphone_get_usage_daily","name":"Get Daily Usage","description":"Get a daily breakdown of usage (messages, calls, webhooks) for the last N days","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"days":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of days to return (1-365, default 30)"}},"hostedApiKey":"none"},"agentphone_get_usage_monthly":{"id":"agentphone_get_usage_monthly","name":"Get Monthly Usage","description":"Get monthly usage aggregation (messages, calls, webhooks) for the last N months","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"months":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of months to return (1-24, default 6)"}},"hostedApiKey":"none"},"agentphone_list_calls":{"id":"agentphone_list_calls","name":"List Calls","description":"List voice calls for this AgentPhone account","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to return (default 20, max 100)"},"offset":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to skip (min 0)"},"status":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter by status (completed, in-progress, failed)"},"direction":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter by direction (inbound, outbound)"},"type":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter by call type (pstn, web)"},"search":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search by phone number (matches fromNumber or toNumber)"}},"hostedApiKey":"none"},"agentphone_list_contacts":{"id":"agentphone_list_contacts","name":"List Contacts","description":"List contacts for this AgentPhone account","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"search":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter by name or phone number (case-insensitive contains)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to return (default 50, max 200)"},"offset":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to skip (min 0)"}},"hostedApiKey":"none"},"agentphone_list_conversations":{"id":"agentphone_list_conversations","name":"List Conversations","description":"List conversations (message threads) for this AgentPhone account","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to return (default 20, max 100)"},"offset":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to skip (min 0)"}},"hostedApiKey":"none"},"agentphone_list_numbers":{"id":"agentphone_list_numbers","name":"List Phone Numbers","description":"List all phone numbers provisioned for this AgentPhone account","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to return (default 20, max 100)"},"offset":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to skip (min 0)"}},"hostedApiKey":"none"},"agentphone_react_to_message":{"id":"agentphone_react_to_message","name":"React to Message","description":"Send an iMessage tapback reaction to a message (iMessage only)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"messageId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the message to react to"},"reaction":{"type":"string","required":true,"visibility":"user-or-llm","description":"Reaction type: love, like, dislike, laugh, emphasize, or question"}},"hostedApiKey":"none"},"agentphone_release_number":{"id":"agentphone_release_number","name":"Release Phone Number","description":"Release (delete) a phone number. This action is irreversible.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"numberId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the phone number to release"}},"hostedApiKey":"none"},"agentphone_send_message":{"id":"agentphone_send_message","name":"Send Message","description":"Send an outbound SMS or iMessage from an AgentPhone agent","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"agentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Agent sending the message"},"toNumber":{"type":"string","required":true,"visibility":"user-or-llm","description":"Recipient phone number in E.164 format (e.g. +14155551234)"},"body":{"type":"string","required":true,"visibility":"user-or-llm","description":"Message text to send"},"mediaUrl":{"type":"string","required":false,"visibility":"user-or-llm","description":"Optional URL of an image, video, or file to attach"},"numberId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Phone number ID to send from. If omitted, the agent\'s first assigned number is used."}},"hostedApiKey":"none"},"agentphone_update_contact":{"id":"agentphone_update_contact","name":"Update Contact","description":"Update a contact\'s fields","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"contactId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Contact ID"},"phoneNumber":{"type":"string","required":false,"visibility":"user-or-llm","description":"New phone number in E.164 format"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"New contact name"},"email":{"type":"string","required":false,"visibility":"user-or-llm","description":"New email address"},"notes":{"type":"string","required":false,"visibility":"user-or-llm","description":"New freeform notes"}},"hostedApiKey":"none"},"agentphone_update_conversation":{"id":"agentphone_update_conversation","name":"Update Conversation","description":"Update conversation metadata (stored state). Pass null to clear existing metadata.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"conversationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Conversation ID"},"metadata":{"type":"json","required":false,"visibility":"user-or-llm","description":"Custom key-value metadata to store on the conversation. Pass null to clear existing metadata."}},"hostedApiKey":"none"},"agiloft_async_status":{"id":"agiloft_async_status","name":"Agiloft Async Status","description":"Check whether an asynchronous Agiloft call, such as a run action button, has completed.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table the asynchronous call was made against"},"callbackId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Callback ID returned by the asynchronous call, e.g. from Run Action Button"}},"hostedApiKey":"none"},"agiloft_attach_file":{"id":"agiloft_attach_file","name":"Agiloft Attach File","description":"Attach a file to a field in an Agiloft record.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to attach the file to"},"fieldName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the attachment field"},"file":{"type":"file","required":true,"visibility":"user-or-llm","description":"File to attach"},"fileName":{"type":"string","required":false,"visibility":"user-or-llm","description":"Name to assign to the file (defaults to original file name)"},"overwrite":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Replace the contents of the field instead of adding another file to it"}},"hostedApiKey":"none"},"agiloft_attachment_info":{"id":"agiloft_attachment_info","name":"Agiloft Attachment Info","description":"Get information about file attachments on a record field.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to check attachments on"},"fieldName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the attachment field to inspect"}},"hostedApiKey":"none"},"agiloft_create_record":{"id":"agiloft_create_record","name":"Agiloft Create Record","description":"Create a new record in an Agiloft table.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"contacts.employees\\")"},"data":{"type":"string","required":true,"visibility":"user-or-llm","description":"Record field values as a JSON object (e.g., {\\"first_name\\": \\"John\\", \\"status\\": \\"Active\\"})"}},"hostedApiKey":"none"},"agiloft_delete_record":{"id":"agiloft_delete_record","name":"Agiloft Delete Record","description":"Delete a record from an Agiloft table.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"contacts.employees\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to delete"},"substituteIds":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated IDs of records that adopt the dependants of the deleted record. Read only when the delete rule is REPLACE_WITH_ANOTHER."},"deleteRule":{"type":"string","required":false,"visibility":"user-or-llm","description":"How to treat records that depend on this one: ERROR_IF_DEPENDANTS (default — fails rather than cascading), APPLY_DELETE_WHERE_POSSIBLE, DELETE_WHERE_POSSIBLE_OTHERWISE_UNLINK, APPLY_UNLINK, UNLINK_WHERE_POSSIBLE_OTHERWISE_DELETE, or REPLACE_WITH_ANOTHER"}},"hostedApiKey":"none"},"agiloft_get_choice_line_id":{"id":"agiloft_get_choice_line_id","name":"Agiloft Get Choice Line ID","description":"Resolve the internal numeric ID of a choice-list value, for use in EWSelect WHERE clauses against choice fields.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"case\\", \\"contracts\\")"},"fieldName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Choice field name (e.g., \\"priority\\", \\"status\\")"},"value":{"type":"string","required":true,"visibility":"user-or-llm","description":"Choice display value to resolve (e.g., \\"High\\", \\"Active\\")"}},"hostedApiKey":"none"},"agiloft_list_tables":{"id":"agiloft_list_tables","name":"Agiloft List Tables","description":"List the tables and fields in an Agiloft knowledge base, to discover the logical names other operations need.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":false,"visibility":"user-or-llm","description":"Logical name of a single table to describe (e.g., \\"contacts\\"). Leave empty to list every table in the knowledge base."},"includeLinkedInfo":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the source table and column behind each linked field"},"skipColumnsInfo":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Return table names only, omitting field details, for a much smaller response"}},"hostedApiKey":"none"},"agiloft_lock_record":{"id":"agiloft_lock_record","name":"Agiloft Lock Record","description":"Lock, unlock, or check the lock status of an Agiloft record.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to lock, unlock, or check"},"lockAction":{"type":"string","required":true,"visibility":"user-or-llm","description":"Action to perform: \\"lock\\", \\"unlock\\", or \\"check\\""},"force":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Unlock only: release a lock held by another user."}},"hostedApiKey":"none"},"agiloft_nlp_search":{"id":"agiloft_nlp_search","name":"Agiloft Natural Language Search","description":"Search Agiloft records by describing what you want in plain language, such as \\"active NDAs submitted last month\\".","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"nlpQuery":{"type":"string","required":true,"visibility":"user-or-llm","description":"The request in plain language, e.g. \\"Show me open, high-priority contracts\\". Structured field filters are not accepted — use Search Records for those."},"fields":{"type":"string","required":true,"visibility":"user-or-llm","description":"Comma-separated field names to return, e.g. \\"id, contract_title1, company_name\\""},"page":{"type":"string","required":false,"visibility":"user-or-llm","description":"Page number, starting from 0"},"limit":{"type":"string","required":false,"visibility":"user-or-llm","description":"Records per page"}},"hostedApiKey":"none"},"agiloft_read_record":{"id":"agiloft_read_record","name":"Agiloft Read Record","description":"Read a record by ID from an Agiloft table.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"contacts.employees\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to read"},"fields":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of field names to include in the response"}},"hostedApiKey":"none"},"agiloft_remove_attachment":{"id":"agiloft_remove_attachment","name":"Agiloft Remove Attachment","description":"Remove an attached file from a field in an Agiloft record.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record containing the attachment"},"fieldName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the attachment field"},"position":{"type":"string","required":true,"visibility":"user-or-llm","description":"Position index of the file to remove (starting from 0)"}},"hostedApiKey":"none"},"agiloft_retrieve_attachment":{"id":"agiloft_retrieve_attachment","name":"Agiloft Retrieve Attachment","description":"Download an attached file from an Agiloft record field.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record containing the attachment"},"fieldName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the attachment field"},"position":{"type":"string","required":true,"visibility":"user-or-llm","description":"Position index of the file in the field (starting from 0)"}},"hostedApiKey":"none"},"agiloft_run_action_button":{"id":"agiloft_run_action_button","name":"Agiloft Run Action Button","description":"Run an action button on an Agiloft record, such as an approval or send-for-signature step.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"case\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to run the action button on"},"actionButtonField":{"type":"string","required":true,"visibility":"user-or-llm","description":"Logical name of the field holding the action button (e.g., \\"ab_field\\")"}},"hostedApiKey":"none"},"agiloft_saved_search":{"id":"agiloft_saved_search","name":"Agiloft Saved Search","description":"List the saved searches defined for an Agiloft table.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Logical table name to list saved searches for (e.g., \\"contract\\")"}},"hostedApiKey":"none"},"agiloft_search_records":{"id":"agiloft_search_records","name":"Agiloft Search Records","description":"Search for records in an Agiloft table using a query.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name to search in (e.g., \\"contracts\\", \\"contacts.employees\\")"},"query":{"type":"string","required":false,"visibility":"user-or-llm","description":"Ad hoc EWSearch query. Combine conditions with && (and) or || (or) and quote every value — e.g. \\"summary~=\'test\'&&priority=\'High\'\\". Required unless a saved search is given."},"search":{"type":"string","required":false,"visibility":"user-or-llm","description":"Label of a saved search defined on the table (e.g., \\"C: Status is Closed\\"). Can be combined with a query to narrow it further."},"fields":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of field names to include in the results"},"page":{"type":"string","required":false,"visibility":"user-or-llm","description":"Page number for paginated results (starting from 0)"},"limit":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum number of records to return per page. Agiloft treats 0 as \\"all records\\", so leave it unset or use a positive value to keep result sizes bounded."}},"hostedApiKey":"none"},"agiloft_select_records":{"id":"agiloft_select_records","name":"Agiloft Select Records","description":"Select record IDs matching a SQL WHERE clause from an Agiloft table.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"contacts.employees\\")"},"where":{"type":"string","required":true,"visibility":"user-or-llm","description":"SQL WHERE clause using database column names (e.g., \\"summary like \'%new%\'\\" or \\"assigned_person=\'John Doe\'\\"). EWSelect has no page size and returns every matching ID, so append a database limit such as \\"limit 0,200\\" to bound the result."}},"hostedApiKey":"none"},"agiloft_update_record":{"id":"agiloft_update_record","name":"Agiloft Update Record","description":"Update an existing record in an Agiloft table.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"contacts.employees\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to update"},"data":{"type":"string","required":true,"visibility":"user-or-llm","description":"Updated field values as a JSON object (e.g., {\\"status\\": \\"Active\\", \\"priority\\": \\"High\\"})"}},"hostedApiKey":"none"},"agiloft_upsert_record":{"id":"agiloft_upsert_record","name":"Agiloft Upsert Record","description":"Create an Agiloft record, or update it when a record already matches the given fields.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"contacts.employees\\")"},"match":{"type":"string","required":true,"visibility":"user-or-llm","description":"Field used to find an existing record (e.g., \\"ext_id\\"). Pick something that identifies a record uniquely — if more than one record matches, Agiloft writes nothing and returns a conflict."},"async":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Queue the write instead of waiting for it. Returns a callback ID instead of a record ID; pass that to Async Status to poll the result."},"data":{"type":"string","required":true,"visibility":"user-or-llm","description":"Field values as a JSON object. On create these populate the new record; on update only the supplied fields change."}},"hostedApiKey":"none"},"ahrefs_anchors":{"id":"ahrefs_anchors","name":"Ahrefs Anchors","description":"Get the anchor text distribution for a target domain or URL\'s backlinks, showing how many links and referring domains use each anchor text.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\" or \\"https://example.com/page\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)"},"history":{"type":"string","required":false,"visibility":"user-or-llm","description":"Historical scope: \\"live\\" (currently live), \\"all_time\\" (default, includes lost backlinks), or \\"since:YYYY-MM-DD\\" (backlinks found since a date)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_backlinks":{"id":"ahrefs_backlinks","name":"Ahrefs Backlinks","description":"Get a list of backlinks pointing to a target domain or URL. Returns details about each backlink including source URL, anchor text, and domain rating.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\" or \\"https://example.com/page\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"history":{"type":"string","required":false,"visibility":"user-or-llm","description":"Historical scope: \\"live\\" (currently live backlinks), \\"all_time\\" (default, includes lost backlinks), or \\"since:YYYY-MM-DD\\" (backlinks found since a date)."},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_backlinks_stats":{"id":"ahrefs_backlinks_stats","name":"Ahrefs Backlinks Stats","description":"Get backlink and referring domain totals for a target domain or URL, both currently live and across all time.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\" or \\"https://example.com/page\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date to report metrics on, in YYYY-MM-DD format (defaults to today)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_batch_analysis":{"id":"ahrefs_batch_analysis","name":"Ahrefs Batch Analysis","description":"Get bulk SEO metrics (Domain Rating, backlinks, referring domains, organic traffic, and more) for multiple domains or URLs in a single request. Useful for comparing many competitors at once.","version":"1.0.0","params":{"targets":{"type":"string","required":true,"visibility":"user-or-llm","description":"Comma-separated list of domains or URLs to analyze. Example: \\"example.com,competitor.com\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode applied to every target: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)"},"protocol":{"type":"string","required":false,"visibility":"user-or-llm","description":"Protocol applied to every target: \\"both\\" (default), \\"http\\", or \\"https\\""},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for traffic data. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"volumeMode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search volume calculation: \\"monthly\\" or \\"average\\" (default: \\"monthly\\")"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_broken_backlinks":{"id":"ahrefs_broken_backlinks","name":"Ahrefs Broken Backlinks","description":"Get a list of broken backlinks pointing to a target domain or URL. Useful for identifying link reclamation opportunities.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\" or \\"https://example.com/page\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_domain_rating":{"id":"ahrefs_domain_rating","name":"Ahrefs Domain Rating","description":"Get the Domain Rating (DR) and Ahrefs Rank for a target domain. Domain Rating shows the strength of a website\'s backlink profile on a scale from 0 to 100.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain to analyze (e.g., example.com)"},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date for historical data in YYYY-MM-DD format (defaults to today)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_domain_rating_history":{"id":"ahrefs_domain_rating_history","name":"Ahrefs Domain Rating History","description":"Get the historical Domain Rating (DR) trend for a target domain or URL over a date range, grouped daily, weekly, or monthly.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"dateFrom":{"type":"string","required":true,"visibility":"user-only","description":"Start date of the historical period, in YYYY-MM-DD format"},"dateTo":{"type":"string","required":false,"visibility":"user-only","description":"End date of the historical period, in YYYY-MM-DD format (defaults to today)"},"historyGrouping":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval for grouping data points: \\"daily\\", \\"weekly\\", or \\"monthly\\" (default: \\"monthly\\")"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_keyword_overview":{"id":"ahrefs_keyword_overview","name":"Ahrefs Keyword Overview","description":"Get detailed metrics for a keyword including search volume, keyword difficulty, CPC, clicks, and traffic potential.","version":"1.0.0","params":{"keyword":{"type":"string","required":true,"visibility":"user-or-llm","description":"The keyword to analyze"},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for keyword data. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_keywords_history":{"id":"ahrefs_keywords_history","name":"Ahrefs Keywords History","description":"Get the historical organic keyword ranking distribution for a target domain or URL over a date range: how many keywords rank in each position bucket at each point in time.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"dateFrom":{"type":"string","required":true,"visibility":"user-only","description":"Start date of the historical period, in YYYY-MM-DD format"},"dateTo":{"type":"string","required":false,"visibility":"user-only","description":"End date of the historical period, in YYYY-MM-DD format (defaults to today)"},"historyGrouping":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval for grouping data points: \\"daily\\", \\"weekly\\", or \\"monthly\\" (default: \\"monthly\\")"},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for search results. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_metrics":{"id":"ahrefs_metrics","name":"Ahrefs Metrics","description":"Get a one-call organic and paid search overview for a target domain or URL: organic traffic, organic keywords, paid traffic, paid keywords, and estimated traffic cost.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for traffic data. Example: \\"us\\", \\"gb\\", \\"de\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date to report metrics on, in YYYY-MM-DD format (defaults to today)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_metrics_history":{"id":"ahrefs_metrics_history","name":"Ahrefs Metrics History","description":"Get the historical organic and paid traffic trend for a target domain or URL over a date range: organic traffic/cost and paid traffic/cost at each point in time.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"dateFrom":{"type":"string","required":true,"visibility":"user-only","description":"Start date of the historical period, in YYYY-MM-DD format"},"dateTo":{"type":"string","required":false,"visibility":"user-only","description":"End date of the historical period, in YYYY-MM-DD format (defaults to today)"},"volumeMode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search volume calculation: \\"monthly\\" or \\"average\\" (default: \\"monthly\\")"},"historyGrouping":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval for grouping data points: \\"daily\\", \\"weekly\\", or \\"monthly\\" (default: \\"monthly\\")"},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for traffic data. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_organic_competitors":{"id":"ahrefs_organic_competitors","name":"Ahrefs Organic Competitors","description":"Get domains that compete with a target domain or URL for the same organic keywords, ranked by keyword overlap.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for search results. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date to report metrics on, in YYYY-MM-DD format (defaults to today)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_organic_keywords":{"id":"ahrefs_organic_keywords","name":"Ahrefs Organic Keywords","description":"Get organic keywords that a target domain or URL ranks for in Google search results. Returns keyword details including search volume, ranking position, and estimated traffic.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\" or \\"https://example.com/page\\""},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for search results. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date to report metrics on, in YYYY-MM-DD format (defaults to today)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_paid_pages":{"id":"ahrefs_paid_pages","name":"Ahrefs Paid Pages","description":"Get a target domain\'s pages that receive paid search traffic, sorted by estimated paid traffic. Returns page URLs with their paid traffic, keyword counts, and estimated spend.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for traffic data. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)"},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date to report metrics on, in YYYY-MM-DD format (defaults to today)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_rank_tracker_competitors_overview":{"id":"ahrefs_rank_tracker_competitors_overview","name":"Ahrefs Rank Tracker Competitors Overview","description":"Get competitor rankings for the keywords tracked in an Ahrefs Rank Tracker project: each tracked keyword\'s volume and difficulty alongside every competitor\'s position, traffic, and traffic value. This endpoint is free and does not consume API units.","version":"1.0.0","params":{"projectId":{"type":"number","required":true,"visibility":"user-or-llm","description":"The Rank Tracker project ID (found in the project URL in Ahrefs)"},"date":{"type":"string","required":true,"visibility":"user-only","description":"Date to report rankings for, in YYYY-MM-DD format"},"device":{"type":"string","required":true,"visibility":"user-or-llm","description":"Rankings device type: \\"desktop\\" or \\"mobile\\""},"dateCompared":{"type":"string","required":false,"visibility":"user-only","description":"Comparison date in YYYY-MM-DD format, to compute position/traffic deltas"},"volumeMode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search volume calculation: \\"monthly\\" or \\"average\\" (default: \\"monthly\\")"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_rank_tracker_competitors_stats":{"id":"ahrefs_rank_tracker_competitors_stats","name":"Ahrefs Rank Tracker Competitors Stats","description":"Get aggregate competitor stats for an Ahrefs Rank Tracker project: each competitor\'s traffic, traffic value, average position, and share of voice across all tracked keywords. This endpoint is free and does not consume API units.","version":"1.0.0","params":{"projectId":{"type":"number","required":true,"visibility":"user-or-llm","description":"The Rank Tracker project ID (found in the project URL in Ahrefs)"},"date":{"type":"string","required":true,"visibility":"user-only","description":"Date to report metrics for, in YYYY-MM-DD format"},"device":{"type":"string","required":true,"visibility":"user-or-llm","description":"Rankings device type: \\"desktop\\" or \\"mobile\\""},"volumeMode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search volume calculation: \\"monthly\\" or \\"average\\" (default: \\"monthly\\")"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_rank_tracker_overview":{"id":"ahrefs_rank_tracker_overview","name":"Ahrefs Rank Tracker Overview","description":"Get ranking overview metrics for the keywords tracked in an Ahrefs Rank Tracker project: position, search volume, keyword difficulty, and estimated traffic. This endpoint is free and does not consume API units.","version":"1.0.0","params":{"projectId":{"type":"number","required":true,"visibility":"user-or-llm","description":"The Rank Tracker project ID (found in the project URL in Ahrefs)"},"date":{"type":"string","required":true,"visibility":"user-only","description":"Date to report rankings for, in YYYY-MM-DD format"},"device":{"type":"string","required":true,"visibility":"user-or-llm","description":"Rankings device type: \\"desktop\\" or \\"mobile\\""},"dateCompared":{"type":"string","required":false,"visibility":"user-only","description":"Comparison date in YYYY-MM-DD format, to compute position/traffic deltas"},"volumeMode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search volume calculation: \\"monthly\\" or \\"average\\" (default: \\"monthly\\")"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_rank_tracker_serp_overview":{"id":"ahrefs_rank_tracker_serp_overview","name":"Ahrefs Rank Tracker SERP Overview","description":"Get the full SERP (search engine results page) for a keyword tracked in an Ahrefs Rank Tracker project, including every ranking URL with its position, title, and authority metrics. This endpoint is free and does not consume API units.","version":"1.0.0","params":{"projectId":{"type":"number","required":true,"visibility":"user-or-llm","description":"The Rank Tracker project ID (found in the project URL in Ahrefs)"},"keyword":{"type":"string","required":true,"visibility":"user-or-llm","description":"The tracked keyword to retrieve SERP data for"},"country":{"type":"string","required":true,"visibility":"user-or-llm","description":"Country code for the tracked keyword. Example: \\"us\\", \\"gb\\", \\"de\\""},"device":{"type":"string","required":true,"visibility":"user-or-llm","description":"Rankings device type: \\"desktop\\" or \\"mobile\\""},"topPositions":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of top organic positions to return (defaults to all available)"},"date":{"type":"string","required":false,"visibility":"user-only","description":"Timestamp to return the last available SERP Overview at, in YYYY-MM-DDThh:mm:ss format"},"locationId":{"type":"number","required":false,"visibility":"user-or-llm","description":"Location ID of the tracked keyword, if tracked at a specific location"},"languageCode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Language code of the tracked keyword"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_refdomains_history":{"id":"ahrefs_refdomains_history","name":"Ahrefs Referring Domains History","description":"Get the historical referring domains trend for a target domain or URL over a date range, grouped daily, weekly, or monthly.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"dateFrom":{"type":"string","required":true,"visibility":"user-only","description":"Start date of the historical period, in YYYY-MM-DD format"},"dateTo":{"type":"string","required":false,"visibility":"user-only","description":"End date of the historical period, in YYYY-MM-DD format (defaults to today)"},"historyGrouping":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval for grouping data points: \\"daily\\", \\"weekly\\", or \\"monthly\\" (default: \\"monthly\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_referring_domains":{"id":"ahrefs_referring_domains","name":"Ahrefs Referring Domains","description":"Get a list of domains that link to a target domain or URL. Returns unique referring domains with their domain rating, backlink counts, and discovery dates.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\" or \\"https://example.com/page\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"history":{"type":"string","required":false,"visibility":"user-or-llm","description":"Historical scope: \\"live\\" (currently live), \\"all_time\\" (default, includes lost domains), or \\"since:YYYY-MM-DD\\" (domains found since a date)."},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_related_terms":{"id":"ahrefs_related_terms","name":"Ahrefs Related Terms","description":"Get keyword ideas related to a seed keyword: terms the same top-ranking pages also rank for (\\"also rank for\\") or also discuss (\\"also talk about\\"), with volume, difficulty, and CPC.","version":"1.0.0","params":{"keyword":{"type":"string","required":true,"visibility":"user-or-llm","description":"The seed keyword to find related terms for"},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for keyword data. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"terms":{"type":"string","required":false,"visibility":"user-or-llm","description":"Type of related keywords to return: \\"also_rank_for\\", \\"also_talk_about\\", or \\"all\\" (default: \\"all\\")"},"viewFor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Whether to derive related terms from the top 10 or top 100 ranking pages (default: \\"top_10\\")"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_site_audit_page_explorer":{"id":"ahrefs_site_audit_page_explorer","name":"Ahrefs Site Audit Page Explorer","description":"Get crawled pages from an Ahrefs Site Audit project with health and SEO metrics: HTTP status, title, link counts, backlinks, indexability, and traffic. Optionally filter to pages affected by a specific issue.","version":"1.0.0","params":{"projectId":{"type":"number","required":true,"visibility":"user-or-llm","description":"The Site Audit project ID (found in the project URL in Ahrefs)"},"date":{"type":"string","required":false,"visibility":"user-only","description":"Crawl date in YYYY-MM-DDThh:mm:ss format (defaults to the most recent crawl)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"offset":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to skip, for pagination"},"issueId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Only return pages affected by this issue ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_top_pages":{"id":"ahrefs_top_pages","name":"Ahrefs Top Pages","description":"Get the top pages of a target domain sorted by organic traffic. Returns page URLs with their traffic, keyword counts, and estimated traffic value.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain to analyze. Example: \\"example.com\\""},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for traffic data. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date to report metrics on, in YYYY-MM-DD format (defaults to today)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"airtable_create_records":{"id":"airtable_create_records","name":"Airtable Create Records","description":"Write new records to an Airtable table","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"records":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of records to create, each with a `fields` object"},"typecast":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"When true, Airtable automatically converts string values to the field type"}},"oauth":{"required":true,"provider":"airtable"},"hostedApiKey":"none"},"airtable_delete_records":{"id":"airtable_delete_records","name":"Airtable Delete Records","description":"Delete one or more records from an Airtable table by ID","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"recordIds":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of record IDs to delete (each starts with \\"rec\\", e.g., [\\"recXXXXXXXXXXXXXX\\"]). Pass a single-element array to delete one record."}},"oauth":{"required":true,"provider":"airtable"},"hostedApiKey":"none"},"airtable_get_base_schema":{"id":"airtable_get_base_schema","name":"Airtable Get Base Schema","description":"Get the schema of all tables, fields, and views in an Airtable base","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"}},"oauth":{"required":true,"provider":"airtable"},"hostedApiKey":"none"},"airtable_get_record":{"id":"airtable_get_record","name":"Airtable Get Record","description":"Retrieve a single record from an Airtable table by its ID","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Record ID to retrieve (starts with \\"rec\\", e.g., \\"recXXXXXXXXXXXXXX\\")"}},"oauth":{"required":true,"provider":"airtable"},"hostedApiKey":"none"},"airtable_list_bases":{"id":"airtable_list_bases","name":"Airtable List Bases","description":"List all bases the authenticated user has access to","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"offset":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination offset for retrieving additional bases"}},"oauth":{"required":true,"provider":"airtable"},"hostedApiKey":"none"},"airtable_list_records":{"id":"airtable_list_records","name":"Airtable List Records","description":"Read records from an Airtable table","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"maxRecords":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of records to return (default: all records)"},"filterFormula":{"type":"string","required":false,"visibility":"user-or-llm","description":"Formula to filter records (e.g., \\"({Field Name} = \'Value\')\\")"}},"oauth":{"required":true,"provider":"airtable"},"hostedApiKey":"none"},"airtable_list_tables":{"id":"airtable_list_tables","name":"Airtable List Tables","description":"List all tables and their schema in an Airtable base","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"}},"oauth":{"required":true,"provider":"airtable"},"hostedApiKey":"none"},"airtable_update_multiple_records":{"id":"airtable_update_multiple_records","name":"Airtable Update Multiple Records","description":"Update multiple existing records in an Airtable table","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"records":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of records to update, each with an `id` and a `fields` object"},"typecast":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"When true, Airtable automatically converts string values to the field type"}},"oauth":{"required":true,"provider":"airtable"},"hostedApiKey":"none"},"airtable_update_record":{"id":"airtable_update_record","name":"Airtable Update Record","description":"Update an existing record in an Airtable table by ID","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Record ID to update (starts with \\"rec\\", e.g., \\"recXXXXXXXXXXXXXX\\")"},"fields":{"type":"json","required":true,"visibility":"user-or-llm","description":"An object containing the field names and their new values"},"typecast":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"When true, Airtable automatically converts string values to the field type"}},"oauth":{"required":true,"provider":"airtable"},"hostedApiKey":"none"},"airtable_upsert_records":{"id":"airtable_upsert_records","name":"Airtable Upsert Records","description":"Update existing records or create new ones in an Airtable table, matching on the specified merge fields","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"records":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of records to upsert, each with a `fields` object"},"fieldsToMergeOn":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of field names used to match existing records (max 3). A record is updated when all merge fields match, otherwise it is created. Example: [\\"Name\\"]"},"typecast":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"When true, Airtable automatically converts string values to the field type"}},"oauth":{"required":true,"provider":"airtable"},"hostedApiKey":"none"},"algolia_add_record":{"id":"algolia_add_record","name":"Algolia Add Record","description":"Add or replace a record in an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"objectID":{"type":"string","required":false,"visibility":"user-or-llm","description":"Object ID for the record (auto-generated if not provided)"},"record":{"type":"json","required":true,"visibility":"user-or-llm","description":"JSON object representing the record to add"}},"hostedApiKey":"none"},"algolia_batch_operations":{"id":"algolia_batch_operations","name":"Algolia Batch Operations","description":"Perform batch add, update, partial update, or delete operations on records in an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"requests":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of batch operations. Each item has \\"action\\" (addObject, updateObject, partialUpdateObject, partialUpdateObjectNoCreate, deleteObject, delete, clear) and \\"body\\" (the record data; must include objectID for update/delete; use an empty object {} for the index-level delete/clear actions)"}},"hostedApiKey":"none"},"algolia_browse_records":{"id":"algolia_browse_records","name":"Algolia Browse Records","description":"Browse and iterate over all records in an Algolia index using cursor pagination","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key (must have browse ACL)"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index to browse"},"query":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search query to filter browsed records"},"filters":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter string to narrow down results"},"attributesToRetrieve":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of attributes to retrieve"},"hitsPerPage":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of hits per page (default: 1000, max: 1000)"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous browse response for pagination"},"aroundLatLng":{"type":"string","required":false,"visibility":"user-or-llm","description":"Coordinates for geo-search (e.g., \\"40.71,-74.01\\")"},"aroundRadius":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum radius in meters for geo-search, or \\"all\\" for unlimited"},"insideBoundingBox":{"type":"json","required":false,"visibility":"user-or-llm","description":"Bounding box coordinates as [[lat1, lng1, lat2, lng2]] for geo-search"},"insidePolygon":{"type":"json","required":false,"visibility":"user-or-llm","description":"Polygon coordinates as [[lat1, lng1, lat2, lng2, lat3, lng3, ...]] for geo-search"}},"hostedApiKey":"none"},"algolia_clear_records":{"id":"algolia_clear_records","name":"Algolia Clear Records","description":"Clear all records from an Algolia index while keeping settings, synonyms, and rules","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key (must have deleteIndex ACL)"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index to clear"}},"hostedApiKey":"none"},"algolia_copy_move_index":{"id":"algolia_copy_move_index","name":"Algolia Copy/Move Index","description":"Copy or move an Algolia index to a new destination","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the source index"},"operation":{"type":"string","required":true,"visibility":"user-or-llm","description":"Operation to perform: \\"copy\\" or \\"move\\""},"destination":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the destination index"},"scope":{"type":"json","required":false,"visibility":"user-or-llm","description":"Array of scopes to copy (only for \\"copy\\" operation): [\\"settings\\", \\"synonyms\\", \\"rules\\"]. Omit to copy everything including records."}},"hostedApiKey":"none"},"algolia_delete_by_filter":{"id":"algolia_delete_by_filter","name":"Algolia Delete By Filter","description":"Delete all records matching a filter from an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key (must have deleteIndex ACL)"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"filters":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter expression to match records for deletion (e.g., \\"category:outdated\\")"},"facetFilters":{"type":"json","required":false,"visibility":"user-or-llm","description":"Array of facet filters (e.g., [\\"brand:Acme\\"])"},"numericFilters":{"type":"json","required":false,"visibility":"user-or-llm","description":"Array of numeric filters (e.g., [\\"price > 100\\"])"},"tagFilters":{"type":"json","required":false,"visibility":"user-or-llm","description":"Array of tag filters using the _tags attribute (e.g., [\\"published\\"])"},"aroundLatLng":{"type":"string","required":false,"visibility":"user-or-llm","description":"Coordinates for geo-search filter (e.g., \\"40.71,-74.01\\")"},"aroundRadius":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum radius in meters for geo-search, or \\"all\\" for unlimited"},"insideBoundingBox":{"type":"json","required":false,"visibility":"user-or-llm","description":"Bounding box coordinates as [[lat1, lng1, lat2, lng2]] for geo-search filter"},"insidePolygon":{"type":"json","required":false,"visibility":"user-or-llm","description":"Polygon coordinates as [[lat1, lng1, lat2, lng2, lat3, lng3, ...]] for geo-search filter"}},"hostedApiKey":"none"},"algolia_delete_index":{"id":"algolia_delete_index","name":"Algolia Delete Index","description":"Delete an entire Algolia index and all its records","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key (must have deleteIndex ACL)"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index to delete"}},"hostedApiKey":"none"},"algolia_delete_record":{"id":"algolia_delete_record","name":"Algolia Delete Record","description":"Delete a record by objectID from an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"objectID":{"type":"string","required":true,"visibility":"user-or-llm","description":"The objectID of the record to delete"}},"hostedApiKey":"none"},"algolia_get_record":{"id":"algolia_get_record","name":"Algolia Get Record","description":"Get a record by objectID from an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"objectID":{"type":"string","required":true,"visibility":"user-or-llm","description":"The objectID of the record to retrieve"},"attributesToRetrieve":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of attributes to retrieve"}},"hostedApiKey":"none"},"algolia_get_records":{"id":"algolia_get_records","name":"Algolia Get Records","description":"Retrieve multiple records by objectID from one or more Algolia indices","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Default index name for all requests"},"requests":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of objects specifying records to retrieve. Each must have \\"objectID\\" and optionally \\"indexName\\" and \\"attributesToRetrieve\\"."}},"hostedApiKey":"none"},"algolia_get_settings":{"id":"algolia_get_settings","name":"Algolia Get Settings","description":"Retrieve the settings of an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"}},"hostedApiKey":"none"},"algolia_get_task_status":{"id":"algolia_get_task_status","name":"Algolia Get Task Status","description":"Check whether an Algolia indexing task has finished publishing","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index the task ran against"},"taskID":{"type":"number","required":true,"visibility":"user-or-llm","description":"The taskID returned by a previous write operation"}},"hostedApiKey":"none"},"algolia_list_indices":{"id":"algolia_list_indices","name":"Algolia List Indices","description":"List all indices in an Algolia application","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key"},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for paginating indices (default: not paginated)"},"hitsPerPage":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of indices per page (default: 100)"}},"hostedApiKey":"none"},"algolia_partial_update_record":{"id":"algolia_partial_update_record","name":"Algolia Partial Update Record","description":"Partially update a record in an Algolia index without replacing it entirely","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"objectID":{"type":"string","required":true,"visibility":"user-or-llm","description":"The objectID of the record to update"},"attributes":{"type":"json","required":true,"visibility":"user-or-llm","description":"JSON object with attributes to update. Supports built-in operations like {\\"stock\\": {\\"_operation\\": \\"Decrement\\", \\"value\\": 1}}"},"createIfNotExists":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Whether to create the record if it does not exist (default: true)"}},"hostedApiKey":"none"},"algolia_search":{"id":"algolia_search","name":"Algolia Search","description":"Search an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index to search"},"query":{"type":"string","required":true,"visibility":"user-or-llm","description":"Search query text"},"hitsPerPage":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of hits per page (default: 20)"},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number to retrieve (default: 0)"},"filters":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter string (e.g., \\"category:electronics AND price < 100\\")"},"attributesToRetrieve":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of attributes to retrieve"},"facets":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of facet attribute names to retrieve counts for (use \\"*\\" for all)"},"getRankingInfo":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Whether to include detailed ranking information in each hit"},"aroundLatLng":{"type":"string","required":false,"visibility":"user-or-llm","description":"Coordinates for geo-search (e.g., \\"40.71,-74.01\\")"},"aroundRadius":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum radius in meters for geo-search, or \\"all\\" for unlimited"},"insideBoundingBox":{"type":"json","required":false,"visibility":"user-or-llm","description":"Bounding box coordinates as [[lat1, lng1, lat2, lng2]] for geo-search"},"insidePolygon":{"type":"json","required":false,"visibility":"user-or-llm","description":"Polygon coordinates as [[lat1, lng1, lat2, lng2, lat3, lng3, ...]] for geo-search"}},"hostedApiKey":"none"},"algolia_update_settings":{"id":"algolia_update_settings","name":"Algolia Update Settings","description":"Update the settings of an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key (must have editSettings ACL)"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"settings":{"type":"json","required":true,"visibility":"user-or-llm","description":"JSON object with settings to update (e.g., {\\"searchableAttributes\\": [\\"name\\", \\"description\\"], \\"customRanking\\": [\\"desc(popularity)\\"]})"},"forwardToReplicas":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Whether to apply changes to replica indices (default: false)"}},"hostedApiKey":"none"},"amplitude_event_segmentation":{"id":"amplitude_event_segmentation","name":"Amplitude Event Segmentation","description":"Query event analytics data with segmentation. Get event counts, uniques, averages, and more.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"eventType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Event type name to analyze"},"start":{"type":"string","required":true,"visibility":"user-or-llm","description":"Start date in YYYYMMDD format"},"end":{"type":"string","required":true,"visibility":"user-or-llm","description":"End date in YYYYMMDD format"},"metric":{"type":"string","required":false,"visibility":"user-or-llm","description":"Metric type: uniques, totals, pct_dau, average, histogram, sums, value_avg, or formula (default: uniques)"},"interval":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval: 1 (daily), 7 (weekly), or 30 (monthly)"},"groupBy":{"type":"string","required":false,"visibility":"user-or-llm","description":"Property name to group by (prefix custom user properties with \\"gp:\\")"},"groupBy2":{"type":"string","required":false,"visibility":"user-or-llm","description":"Second property name to group by (prefix custom user properties with \\"gp:\\")"},"limit":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum number of group-by values (max 1000)"},"filters":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON array of filter objects applied to the event, e.g. [{\\"subprop_type\\":\\"event\\",\\"subprop_key\\":\\"city\\",\\"subprop_op\\":\\"is\\",\\"subprop_value\\":[\\"San Francisco\\"]}]"},"formula":{"type":"string","required":false,"visibility":"user-or-llm","description":"Required when metric is \\"formula\\", e.g. \\"UNIQUES(A)/UNIQUES(B)\\""},"segment":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON segment definition(s) applied to the query"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}},"hostedApiKey":"none"},"amplitude_funnels":{"id":"amplitude_funnels","name":"Amplitude Funnels","description":"Analyze conversion rates and drop-off between a sequence of events.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"events":{"type":"string","required":true,"visibility":"user-or-llm","description":"JSON array of event objects, one per funnel step in order, e.g. [{\\"event_type\\":\\"signup\\"},{\\"event_type\\":\\"purchase\\"}]"},"start":{"type":"string","required":true,"visibility":"user-or-llm","description":"Start date in YYYYMMDD format"},"end":{"type":"string","required":true,"visibility":"user-or-llm","description":"End date in YYYYMMDD format"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Funnel ordering: \\"ordered\\", \\"unordered\\", or \\"sequential\\" (default: ordered)"},"userType":{"type":"string","required":false,"visibility":"user-or-llm","description":"User type: \\"new\\" or \\"active\\" (default: active)"},"interval":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval: -300000 (real-time), -3600000 (hourly), 1 (daily), 7 (weekly), or 30 (monthly)"},"conversionWindowSeconds":{"type":"string","required":false,"visibility":"user-or-llm","description":"Conversion window in seconds (default: 2592000, i.e. 30 days)"},"groupBy":{"type":"string","required":false,"visibility":"user-or-llm","description":"Property to group by (limit: one; prefix custom properties with \\"gp:\\")"},"limit":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum number of group-by values (default: 100, max: 1000)"},"segment":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON segment definition(s) applied to the query"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}},"hostedApiKey":"none"},"amplitude_get_active_users":{"id":"amplitude_get_active_users","name":"Amplitude Get Active Users","description":"Get active or new user counts over a date range from the Dashboard REST API.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"start":{"type":"string","required":true,"visibility":"user-or-llm","description":"Start date in YYYYMMDD format"},"end":{"type":"string","required":true,"visibility":"user-or-llm","description":"End date in YYYYMMDD format"},"metric":{"type":"string","required":false,"visibility":"user-or-llm","description":"Metric type: \\"active\\" or \\"new\\" (default: active)"},"interval":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval: 1 (daily), 7 (weekly), or 30 (monthly)"},"groupBy":{"type":"string","required":false,"visibility":"user-or-llm","description":"Property name to group by"},"segment":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON segment definition(s) applied to the query"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}},"hostedApiKey":"none"},"amplitude_get_revenue":{"id":"amplitude_get_revenue","name":"Amplitude Get Revenue","description":"Get revenue LTV data including ARPU, ARPPU, total revenue, and paying user counts.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"start":{"type":"string","required":true,"visibility":"user-or-llm","description":"Start date in YYYYMMDD format"},"end":{"type":"string","required":true,"visibility":"user-or-llm","description":"End date in YYYYMMDD format"},"metric":{"type":"string","required":false,"visibility":"user-or-llm","description":"Metric: 0 (ARPU), 1 (ARPPU), 2 (Total Revenue), 3 (Paying Users)"},"interval":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval: 1 (daily), 7 (weekly), or 30 (monthly)"},"groupBy":{"type":"string","required":false,"visibility":"user-or-llm","description":"Property name to group by (limit: one)"},"segment":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON segment definition(s) applied to the query"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}},"hostedApiKey":"none"},"amplitude_group_identify":{"id":"amplitude_group_identify","name":"Amplitude Group Identify","description":"Set group-level properties in Amplitude. Supports $set, $setOnce, $add, $append, $unset operations.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"groupType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Group classification (e.g., \\"company\\", \\"org_id\\")"},"groupValue":{"type":"string","required":true,"visibility":"user-or-llm","description":"Specific group identifier (e.g., \\"Acme Corp\\")"},"groupProperties":{"type":"string","required":true,"visibility":"user-or-llm","description":"JSON object of group properties. Use operations like $set, $setOnce, $add, $append, $unset."},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}},"hostedApiKey":"none"},"amplitude_identify_user":{"id":"amplitude_identify_user","name":"Amplitude Identify User","description":"Set user properties in Amplitude using the Identify API. Supports $set, $setOnce, $add, $append, $unset operations.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"userId":{"type":"string","required":false,"visibility":"user-or-llm","description":"User ID (required if no device_id)"},"deviceId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Device ID (required if no user_id)"},"userProperties":{"type":"string","required":true,"visibility":"user-or-llm","description":"JSON object of user properties. Use operations like $set, $setOnce, $add, $append, $unset."},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}},"hostedApiKey":"none"},"amplitude_list_events":{"id":"amplitude_list_events","name":"Amplitude List Events","description":"List all event types in the Amplitude project with their weekly totals and unique counts.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}},"hostedApiKey":"none"},"amplitude_realtime_active_users":{"id":"amplitude_realtime_active_users","name":"Amplitude Real-time Active Users","description":"Get real-time active user counts at 5-minute granularity for the last 2 days.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}},"hostedApiKey":"none"},"amplitude_retention":{"id":"amplitude_retention","name":"Amplitude Retention","description":"Measure how many users return to perform an action after a starting action.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"startEvent":{"type":"string","required":true,"visibility":"user-or-llm","description":"JSON starting event object, e.g. {\\"event_type\\":\\"_new\\"} or {\\"event_type\\":\\"_active\\"}"},"returnEvent":{"type":"string","required":true,"visibility":"user-or-llm","description":"JSON returning event object, e.g. {\\"event_type\\":\\"_all\\"} or {\\"event_type\\":\\"_active\\"}"},"start":{"type":"string","required":true,"visibility":"user-or-llm","description":"Start date in YYYYMMDD format"},"end":{"type":"string","required":true,"visibility":"user-or-llm","description":"End date in YYYYMMDD format"},"retentionMode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Retention type: \\"bracket\\", \\"rolling\\", or \\"n-day\\" (default: n-day)"},"retentionBrackets":{"type":"string","required":false,"visibility":"user-or-llm","description":"Required when Retention Mode is \\"bracket\\". Day ranges, e.g. [[0,4]]"},"interval":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval: 1 (daily), 7 (weekly), or 30 (monthly)"},"groupBy":{"type":"string","required":false,"visibility":"user-or-llm","description":"Property to group by (limit: one; prefix custom properties with \\"gp:\\")"},"segment":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON segment definition(s) applied to the query"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}},"hostedApiKey":"none"},"amplitude_send_event":{"id":"amplitude_send_event","name":"Amplitude Send Event","description":"Track an event in Amplitude using the HTTP V2 API.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"userId":{"type":"string","required":false,"visibility":"user-or-llm","description":"User ID (required if no device_id)"},"deviceId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Device ID (required if no user_id)"},"eventType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the event (e.g., \\"page_view\\", \\"purchase\\")"},"eventProperties":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON object of custom event properties"},"userProperties":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON object of user properties to set (supports $set, $setOnce, $add, $append, $unset)"},"time":{"type":"string","required":false,"visibility":"user-or-llm","description":"Event timestamp in milliseconds since epoch"},"sessionId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Session start time in milliseconds since epoch"},"insertId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Unique ID for deduplication (within 7-day window)"},"appVersion":{"type":"string","required":false,"visibility":"user-or-llm","description":"Application version string"},"platform":{"type":"string","required":false,"visibility":"user-or-llm","description":"Platform (e.g., \\"Web\\", \\"iOS\\", \\"Android\\")"},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Two-letter country code"},"language":{"type":"string","required":false,"visibility":"user-or-llm","description":"Language code (e.g., \\"en\\")"},"ip":{"type":"string","required":false,"visibility":"user-or-llm","description":"IP address for geo-location"},"price":{"type":"string","required":false,"visibility":"user-or-llm","description":"Price of the item purchased"},"quantity":{"type":"string","required":false,"visibility":"user-or-llm","description":"Quantity of items purchased"},"revenue":{"type":"string","required":false,"visibility":"user-or-llm","description":"Revenue amount"},"productId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Product identifier"},"revenueType":{"type":"string","required":false,"visibility":"user-or-llm","description":"Revenue type (e.g., \\"purchase\\", \\"refund\\")"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}},"hostedApiKey":"none"},"amplitude_user_activity":{"id":"amplitude_user_activity","name":"Amplitude User Activity","description":"Get the event stream for a specific user by their Amplitude ID.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"amplitudeId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Amplitude internal user ID"},"offset":{"type":"string","required":false,"visibility":"user-or-llm","description":"Offset for pagination (default 0)"},"limit":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum number of events to return (default 1000, max 1000)"},"direction":{"type":"string","required":false,"visibility":"user-or-llm","description":"Sort direction: \\"latest\\" or \\"earliest\\" (default: latest)"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}},"hostedApiKey":"none"},"amplitude_user_profile":{"id":"amplitude_user_profile","name":"Amplitude User Profile","description":"Get a user profile including properties, cohort memberships, and computed properties. Not available for EU data-residency projects.","version":"1.0.0","params":{"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"userId":{"type":"string","required":false,"visibility":"user-or-llm","description":"External user ID (required if no device_id)"},"deviceId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Device ID (required if no user_id)"},"getAmpProps":{"type":"string","required":false,"visibility":"user-or-llm","description":"Include Amplitude user properties (true/false, default: false)"},"getCohortIds":{"type":"string","required":false,"visibility":"user-or-llm","description":"Include cohort IDs the user belongs to (true/false, default: false)"},"getComputations":{"type":"string","required":false,"visibility":"user-or-llm","description":"Include computed user properties (true/false, default: false)"}},"hostedApiKey":"none"},"amplitude_user_search":{"id":"amplitude_user_search","name":"Amplitude User Search","description":"Search for a user by User ID, Device ID, or Amplitude ID using the Dashboard REST API.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"user":{"type":"string","required":true,"visibility":"user-or-llm","description":"User ID, Device ID, or Amplitude ID to search for"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}},"hostedApiKey":"none"},"apify_get_dataset_items":{"id":"apify_get_dataset_items","name":"APIFY Get Dataset Items","description":"Retrieve items stored in an APIFY dataset","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"APIFY API token from console.apify.com/account#/integrations"},"datasetId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Dataset ID to read items from. Example: \\"9RnD3Pql2vGZkc5H5\\""},"itemLimit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Max items to return (1-250000). Default: all items. Example: 500"},"offset":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to skip at the start. Default: 0"},"fields":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of fields to include. Example: \\"title,url,price\\""}},"hostedApiKey":"none"},"apify_get_run":{"id":"apify_get_run","name":"APIFY Get Run","description":"Get the status and details of an APIFY actor run","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"APIFY API token from console.apify.com/account#/integrations"},"runId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Actor run ID to fetch. Example: \\"HG7ML7M8z78YcAPEB\\""}},"hostedApiKey":"none"},"apify_run_actor_async":{"id":"apify_run_actor_async","name":"APIFY Run Actor (Async)","description":"Start an APIFY Actor run and poll it within the same workflow step until completion or the polling deadline","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"APIFY API token from console.apify.com/account#/integrations"},"actorId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Actor ID or username/actor-name. Examples: \\"apify/web-scraper\\", \\"janedoe/my-actor\\", \\"moJRLRc85AitArpNN\\""},"input":{"type":"string","required":false,"visibility":"user-or-llm","description":"Actor input as JSON string. Example: { \\"startUrls\\": [ { \\"url\\": \\"https://example.com\\" } ], \\"maxPages\\": 10 }"},"waitForFinish":{"type":"number","required":false,"visibility":"user-or-llm","description":"Initial wait time in seconds (0-60) before polling starts. Example: 30"},"itemLimit":{"type":"number","required":false,"default":100,"visibility":"user-or-llm","description":"Max dataset items to fetch (1-250000). Default: 100. Example: 500"},"memory":{"type":"number","required":false,"visibility":"user-or-llm","description":"Memory in megabytes allocated for the actor run (128-32768). Example: 1024 for 1GB, 2048 for 2GB"},"timeout":{"type":"number","required":false,"visibility":"user-or-llm","description":"Timeout in seconds for the actor run. Example: 300 for 5 minutes, 3600 for 1 hour"},"build":{"type":"string","required":false,"visibility":"user-or-llm","description":"Actor build to run. Examples: \\"latest\\", \\"beta\\", \\"1.2.3\\", \\"build-tag-name\\""}},"hostedApiKey":"none"},"apify_run_actor_sync":{"id":"apify_run_actor_sync","name":"APIFY Run Actor (Sync)","description":"Run an APIFY actor synchronously and get results (max 5 minutes)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"APIFY API token from console.apify.com/account#/integrations"},"actorId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Actor ID or username/actor-name. Examples: \\"apify/web-scraper\\", \\"janedoe/my-actor\\", \\"moJRLRc85AitArpNN\\""},"input":{"type":"string","required":false,"visibility":"user-or-llm","description":"Actor input as JSON string. Example: { \\"startUrls\\": [ { \\"url\\": \\"https://example.com\\" } ], \\"maxPages\\": 10 }"},"memory":{"type":"number","required":false,"visibility":"user-or-llm","description":"Memory in megabytes allocated for the actor run (128-32768). Example: 1024 for 1GB, 2048 for 2GB"},"timeout":{"type":"number","required":false,"visibility":"user-or-llm","description":"Timeout in seconds for the actor run. Example: 300 for 5 minutes, 3600 for 1 hour"},"build":{"type":"string","required":false,"visibility":"user-or-llm","description":"Actor build to run. Examples: \\"latest\\", \\"beta\\", \\"1.2.3\\", \\"build-tag-name\\""}},"hostedApiKey":"none"},"apify_run_task":{"id":"apify_run_task","name":"APIFY Run Task","description":"Run a saved APIFY actor task synchronously and get dataset items (max 5 minutes)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"APIFY API token from console.apify.com/account#/integrations"},"taskId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Task ID or username/task-name. Examples: \\"janedoe/my-task\\", \\"moJRLRc85AitArpNN\\""},"input":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON string that overrides the task\'s saved input. Example: { \\"startUrls\\": [ { \\"url\\": \\"https://example.com\\" } ] }"},"itemLimit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Max dataset items to return (1-250000). Example: 500"},"memory":{"type":"number","required":false,"visibility":"user-or-llm","description":"Memory in megabytes allocated for the run (128-32768). Example: 1024 for 1GB"},"timeout":{"type":"number","required":false,"visibility":"user-or-llm","description":"Timeout in seconds for the run. Example: 300 for 5 minutes"},"build":{"type":"string","required":false,"visibility":"user-or-llm","description":"Actor build to run. Examples: \\"latest\\", \\"beta\\", \\"1.2.3\\""}},"hostedApiKey":"none"},"apollo_account_bulk_create":{"id":"apollo_account_bulk_create","name":"Apollo Bulk Create Accounts","description":"Create up to 100 accounts at once in your Apollo database. Set run_dedupe=true to deduplicate by domain, organization_id, and name. Master key required.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"accounts":{"type":"array","required":true,"visibility":"user-or-llm","description":"Array of accounts to create (max 100). Each account should include a name, and may optionally include domain, phone, phone_status_cd, raw_address, owner_id, linkedin_url, facebook_url, twitter_url, salesforce_id, and hubspot_id."},"append_label_names":{"type":"array","required":false,"visibility":"user-only","description":"Array of label names to add to ALL accounts in this request"},"run_dedupe":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"When true, performs aggressive deduplication by domain, organization_id, and name (defaults to false)"}},"hostedApiKey":"none"},"apollo_account_bulk_update":{"id":"apollo_account_bulk_update","name":"Apollo Bulk Update Accounts","description":"Update up to 1000 existing accounts at once in your Apollo database (higher limit than contacts!). Each account must include an id field. Master key required.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"account_ids":{"type":"array","required":false,"visibility":"user-or-llm","description":"Array of account IDs to update with the same values (max 1000). Use with name/owner_id for uniform updates. Use either this OR account_attributes."},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"When using account_ids, apply this name to all accounts"},"owner_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"When using account_ids, apply this owner to all accounts"},"account_stage_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"When using account_ids, apply this account stage to all accounts"},"account_attributes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Array of account objects with individual updates (each must include id). Example: [{\\"id\\": \\"acc1\\", \\"name\\": \\"Acme\\", \\"owner_id\\": \\"u1\\", \\"account_stage_id\\": \\"s1\\", \\"typed_custom_fields\\": {\\"field_id\\": \\"value\\"}}]"},"async":{"type":"boolean","required":false,"visibility":"user-only","description":"When true, processes the update asynchronously. Only supported when using account_ids; returns 422 if used with account_attributes."}},"hostedApiKey":"none"},"apollo_account_create":{"id":"apollo_account_create","name":"Apollo Create Account","description":"Create a new account (company) in your Apollo database","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Company name (e.g., \\"Acme Corporation\\")"},"domain":{"type":"string","required":false,"visibility":"user-or-llm","description":"Company domain without www. prefix (e.g., \\"acme.com\\")"},"phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Primary phone number for the account"},"owner_id":{"type":"string","required":false,"visibility":"user-only","description":"Apollo user ID of the account owner"},"account_stage_id":{"type":"string","required":false,"visibility":"user-only","description":"Apollo ID for the account stage to assign this account to"},"raw_address":{"type":"string","required":false,"visibility":"user-or-llm","description":"Corporate location (e.g., \\"San Francisco, CA, USA\\")"},"typed_custom_fields":{"type":"json","required":false,"visibility":"user-only","description":"Custom field values as { custom_field_id: value } map"}},"hostedApiKey":"none"},"apollo_account_search":{"id":"apollo_account_search","name":"Apollo Search Accounts","description":"Search your team\'s accounts in Apollo. Display limit: 50,000 records (100 records per page, 500 pages max). Use filters to narrow results. Master key required.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"q_organization_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter accounts by organization name (partial-match search)"},"account_stage_ids":{"type":"array","required":false,"visibility":"user-only","description":"Filter by account stage IDs"},"account_label_ids":{"type":"array","required":false,"visibility":"user-only","description":"Filter by account label IDs"},"sort_by_field":{"type":"string","required":false,"visibility":"user-or-llm","description":"Sort field: \\"account_last_activity_date\\", \\"account_created_at\\", or \\"account_updated_at\\""},"sort_ascending":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Sort ascending when true. Defaults to descending."},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, max 100 (e.g., 25, 50, 100)"}},"hostedApiKey":"none"},"apollo_account_update":{"id":"apollo_account_update","name":"Apollo Update Account","description":"Update an existing account in your Apollo database","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"account_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the account to update (e.g., \\"acc_abc123\\")"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Company name (e.g., \\"Acme Corporation\\")"},"domain":{"type":"string","required":false,"visibility":"user-or-llm","description":"Company domain (e.g., \\"acme.com\\")"},"phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Company phone number"},"owner_id":{"type":"string","required":false,"visibility":"user-only","description":"Apollo user ID of the account owner"},"account_stage_id":{"type":"string","required":false,"visibility":"user-only","description":"Apollo ID for the account stage to assign this account to"},"raw_address":{"type":"string","required":false,"visibility":"user-or-llm","description":"Corporate location (e.g., \\"San Francisco, CA, USA\\")"},"typed_custom_fields":{"type":"json","required":false,"visibility":"user-only","description":"Custom field values as { custom_field_id: value } map"}},"hostedApiKey":"none"},"apollo_contact_bulk_create":{"id":"apollo_contact_bulk_create","name":"Apollo Bulk Create Contacts","description":"Create up to 100 contacts at once in your Apollo database. Supports deduplication to prevent creating duplicate contacts. Master key required.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"contacts":{"type":"array","required":true,"visibility":"user-or-llm","description":"Array of contacts to create (max 100). Each contact may include first_name, last_name, email, title, organization_name, account_id, owner_id, contact_stage_id, linkedin_url, phone (single string) or phone_numbers (array of {raw_number, position}), contact_emails, typed_custom_fields, and CRM IDs (salesforce_contact_id, hubspot_id, team_id) for cross-system matching"},"append_label_names":{"type":"array","required":false,"visibility":"user-or-llm","description":"Label names to add to all contacts in this request (e.g., [\\"Hot Lead\\"])"},"run_dedupe":{"type":"boolean","required":false,"visibility":"user-only","description":"Enable deduplication to prevent creating duplicate contacts. When true, existing contacts are returned without modification"}},"hostedApiKey":"none"},"apollo_contact_bulk_update":{"id":"apollo_contact_bulk_update","name":"Apollo Bulk Update Contacts","description":"Update up to 100 existing contacts at once in your Apollo database. Each contact must include an id field. Master key required.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"contact_ids":{"type":"array","required":false,"visibility":"user-or-llm","description":"Array of contact IDs to update. Must be paired with an object-form contact_attributes specifying the fields to apply uniformly to all listed contacts."},"contact_attributes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Required. Either an array of per-contact updates (each with id) — used standalone — or a single object of attributes to apply to all contact_ids. Supported fields: owner_id, email, organization_name, title, first_name, last_name, account_id, present_raw_address, linkedin_url, typed_custom_fields"},"async":{"type":"boolean","required":false,"visibility":"user-only","description":"Force asynchronous processing. Automatically enabled for >100 contacts"}},"hostedApiKey":"none"},"apollo_contact_create":{"id":"apollo_contact_create","name":"Apollo Create Contact","description":"Create a new contact in your Apollo database","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"first_name":{"type":"string","required":true,"visibility":"user-or-llm","description":"First name of the contact"},"last_name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Last name of the contact"},"email":{"type":"string","required":false,"visibility":"user-or-llm","description":"Email address of the contact"},"title":{"type":"string","required":false,"visibility":"user-or-llm","description":"Job title (e.g., \\"VP of Sales\\", \\"Software Engineer\\")"},"account_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"Apollo account ID to associate with (e.g., \\"acc_abc123\\")"},"owner_id":{"type":"string","required":false,"visibility":"user-only","description":"User ID of the contact owner (accepted by Apollo but not officially documented for POST /contacts)"},"organization_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Name of the contact\'s employer (e.g., \\"Apollo\\")"},"website_url":{"type":"string","required":false,"visibility":"user-or-llm","description":"Corporate website URL (e.g., \\"https://www.apollo.io/\\")"},"label_names":{"type":"array","required":false,"visibility":"user-or-llm","description":"Lists/labels to add the contact to (e.g., [\\"Prospects\\"])"},"contact_stage_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"Apollo ID for the contact stage"},"present_raw_address":{"type":"string","required":false,"visibility":"user-or-llm","description":"Personal location for the contact (e.g., \\"Atlanta, United States\\")"},"direct_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Primary phone number"},"corporate_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Work/office phone number"},"mobile_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Mobile phone number"},"home_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Home phone number"},"other_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Alternative phone number"},"typed_custom_fields":{"type":"json","required":false,"visibility":"user-or-llm","description":"Custom field values keyed by custom field ID"},"run_dedupe":{"type":"boolean","required":false,"visibility":"user-only","description":"When true, Apollo deduplicates against existing contacts"}},"hostedApiKey":"none"},"apollo_contact_search":{"id":"apollo_contact_search","name":"Apollo Search Contacts","description":"Search your team\'s contacts in Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"q_keywords":{"type":"string","required":false,"visibility":"user-or-llm","description":"Keywords to search for"},"contact_stage_ids":{"type":"array","required":false,"visibility":"user-only","description":"Filter by contact stage IDs"},"contact_label_ids":{"type":"array","required":false,"visibility":"user-only","description":"Filter by Apollo label IDs (lists)"},"sort_by_field":{"type":"string","required":false,"visibility":"user-only","description":"Sort field: contact_last_activity_date, contact_email_last_opened_at, contact_email_last_clicked_at, contact_created_at, or contact_updated_at"},"sort_ascending":{"type":"boolean","required":false,"visibility":"user-only","description":"When true, sort ascending. Must be used together with sort_by_field"},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, max 100 (e.g., 25, 50, 100)"}},"hostedApiKey":"none"},"apollo_contact_update":{"id":"apollo_contact_update","name":"Apollo Update Contact","description":"Update an existing contact in your Apollo database","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"contact_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the contact to update (e.g., \\"con_abc123\\")"},"first_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"First name of the contact"},"last_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Last name of the contact"},"email":{"type":"string","required":false,"visibility":"user-or-llm","description":"Email address"},"title":{"type":"string","required":false,"visibility":"user-or-llm","description":"Job title (e.g., \\"VP of Sales\\", \\"Software Engineer\\")"},"account_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"Apollo account ID (e.g., \\"acc_abc123\\")"},"owner_id":{"type":"string","required":false,"visibility":"user-only","description":"User ID of the contact owner (accepted by Apollo but not officially documented for PATCH /contacts/{id})"},"organization_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Name of the contact\'s employer (e.g., \\"Apollo\\")"},"website_url":{"type":"string","required":false,"visibility":"user-or-llm","description":"Corporate website URL (e.g., \\"https://www.apollo.io/\\")"},"label_names":{"type":"array","required":false,"visibility":"user-or-llm","description":"Lists/labels to add the contact to (e.g., [\\"Prospects\\"])"},"contact_stage_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"Apollo ID for the contact stage"},"present_raw_address":{"type":"string","required":false,"visibility":"user-or-llm","description":"Personal location for the contact (e.g., \\"Atlanta, United States\\")"},"direct_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Primary phone number"},"corporate_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Work/office phone number"},"mobile_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Mobile phone number"},"home_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Home phone number"},"other_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Alternative phone number"},"typed_custom_fields":{"type":"json","required":false,"visibility":"user-or-llm","description":"Custom field values keyed by custom field ID"}},"hostedApiKey":"none"},"apollo_email_accounts":{"id":"apollo_email_accounts","name":"Apollo Get Email Accounts","description":"Get list of team\'s linked email accounts in Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"}},"hostedApiKey":"none"},"apollo_opportunity_create":{"id":"apollo_opportunity_create","name":"Apollo Create Opportunity","description":"Create a new deal for an account in your Apollo database (master key required)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the opportunity/deal (e.g., \\"Enterprise License - Q1\\")"},"account_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"ID of the account this opportunity belongs to (e.g., \\"acc_abc123\\")"},"amount":{"type":"string","required":false,"visibility":"user-or-llm","description":"Monetary value as a plain number string with no commas or currency symbols"},"opportunity_stage_id":{"type":"string","required":false,"visibility":"user-only","description":"ID of the opportunity stage"},"owner_id":{"type":"string","required":false,"visibility":"user-only","description":"User ID of the opportunity owner"},"closed_date":{"type":"string","required":false,"visibility":"user-or-llm","description":"Expected close date in YYYY-MM-DD format"},"typed_custom_fields":{"type":"json","required":false,"visibility":"user-only","description":"Custom field values as { custom_field_id: value } map"}},"hostedApiKey":"none"},"apollo_opportunity_get":{"id":"apollo_opportunity_get","name":"Apollo Get Opportunity","description":"Retrieve complete details of a specific deal/opportunity by ID","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"opportunity_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the opportunity to retrieve (e.g., \\"opp_abc123\\")"}},"hostedApiKey":"none"},"apollo_opportunity_search":{"id":"apollo_opportunity_search","name":"Apollo Search Opportunities","description":"Search and list all deals/opportunities in your team\'s Apollo account","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"sort_by_field":{"type":"string","required":false,"visibility":"user-or-llm","description":"Sort field: \\"amount\\", \\"is_closed\\", or \\"is_won\\""},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, max 100 (e.g., 25, 50, 100)"}},"hostedApiKey":"none"},"apollo_opportunity_update":{"id":"apollo_opportunity_update","name":"Apollo Update Opportunity","description":"Update an existing deal/opportunity in your Apollo database","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"opportunity_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the opportunity to update (e.g., \\"opp_abc123\\")"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Name of the opportunity/deal (e.g., \\"Enterprise License - Q1\\")"},"amount":{"type":"string","required":false,"visibility":"user-or-llm","description":"Monetary value as a plain number string with no commas or currency symbols"},"opportunity_stage_id":{"type":"string","required":false,"visibility":"user-only","description":"ID of the opportunity stage"},"owner_id":{"type":"string","required":false,"visibility":"user-only","description":"User ID of the opportunity owner"},"closed_date":{"type":"string","required":false,"visibility":"user-or-llm","description":"Expected close date in YYYY-MM-DD format"},"typed_custom_fields":{"type":"json","required":false,"visibility":"user-only","description":"Custom field values as { custom_field_id: value } map"}},"hostedApiKey":"none"},"apollo_organization_bulk_enrich":{"id":"apollo_organization_bulk_enrich","name":"Apollo Bulk Organization Enrichment","description":"Enrich data for up to 10 organizations at once using Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"domains":{"type":"array","required":true,"visibility":"user-or-llm","description":"Array of company domains to enrich (max 10, no www. or @, e.g., [\\"apollo.io\\", \\"stripe.com\\"])"}},"hostedApiKey":"none"},"apollo_organization_enrich":{"id":"apollo_organization_enrich","name":"Apollo Organization Enrichment","description":"Enrich data for a single organization using Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"domain":{"type":"string","required":true,"visibility":"user-or-llm","description":"Company domain (e.g., \\"apollo.io\\", \\"acme.com\\")"}},"hostedApiKey":"none"},"apollo_organization_search":{"id":"apollo_organization_search","name":"Apollo Organization Search","description":"Search Apollo\'s database for companies using filters","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"organization_locations":{"type":"array","required":false,"visibility":"user-or-llm","description":"Company HQ locations (cities, US states, or countries)"},"organization_not_locations":{"type":"array","required":false,"visibility":"user-or-llm","description":"Exclude companies whose HQ is in these locations"},"organization_num_employees_ranges":{"type":"array","required":false,"visibility":"user-or-llm","description":"Employee count ranges as \\"min,max\\" strings (e.g., [\\"1,10\\", \\"250,500\\", \\"10000,20000\\"])"},"q_organization_keyword_tags":{"type":"array","required":false,"visibility":"user-or-llm","description":"Industry or keyword tags"},"q_organization_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Organization name to search for (e.g., \\"Acme\\", \\"TechCorp\\")"},"organization_ids":{"type":"array","required":false,"visibility":"user-or-llm","description":"Apollo organization IDs to include (e.g., [\\"5e66b6381e05b4008c8331b8\\"])"},"q_organization_domains_list":{"type":"array","required":false,"visibility":"user-or-llm","description":"Domain names to filter by (no www. or @, up to 1,000)"},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, max 100 (e.g., 25, 50, 100)"}},"hostedApiKey":"none"},"apollo_people_bulk_enrich":{"id":"apollo_people_bulk_enrich","name":"Apollo Bulk People Enrichment","description":"Enrich data for up to 10 people at once using Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"people":{"type":"array","required":true,"visibility":"user-or-llm","description":"Array of people to enrich (max 10)"},"reveal_personal_emails":{"type":"boolean","required":false,"visibility":"user-only","description":"Reveal personal email addresses (uses credits)"},"reveal_phone_number":{"type":"boolean","required":false,"visibility":"user-only","description":"Reveal phone numbers (uses credits, requires webhook_url)"},"webhook_url":{"type":"string","required":false,"visibility":"user-only","description":"Webhook URL for async phone number delivery (required when reveal_phone_number is true)"}},"hostedApiKey":"none"},"apollo_people_enrich":{"id":"apollo_people_enrich","name":"Apollo People Enrichment","description":"Enrich data for a single person using Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"first_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"First name of the person"},"last_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Last name of the person"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Full name of the person (alternative to first_name/last_name)"},"id":{"type":"string","required":false,"visibility":"user-or-llm","description":"Apollo ID for the person"},"hashed_email":{"type":"string","required":false,"visibility":"user-or-llm","description":"MD5 or SHA-256 hashed email"},"email":{"type":"string","required":false,"visibility":"user-or-llm","description":"Email address of the person"},"organization_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Company name where the person works"},"domain":{"type":"string","required":false,"visibility":"user-or-llm","description":"Company domain (e.g., \\"apollo.io\\", \\"acme.com\\")"},"linkedin_url":{"type":"string","required":false,"visibility":"user-or-llm","description":"LinkedIn profile URL"},"reveal_personal_emails":{"type":"boolean","required":false,"visibility":"user-only","description":"Reveal personal email addresses (uses credits)"},"reveal_phone_number":{"type":"boolean","required":false,"visibility":"user-only","description":"Reveal phone numbers (uses credits, requires webhook_url)"},"webhook_url":{"type":"string","required":false,"visibility":"user-only","description":"Webhook URL for async phone number delivery (required when reveal_phone_number is true)"}},"hostedApiKey":"none"},"apollo_people_search":{"id":"apollo_people_search","name":"Apollo People Search","description":"Search Apollo\'s database for people using demographic filters","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"person_titles":{"type":"array","required":false,"visibility":"user-or-llm","description":"Job titles to search for (e.g., [\\"CEO\\", \\"VP of Sales\\"])"},"include_similar_titles":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Whether to return people with job titles similar to person_titles"},"person_locations":{"type":"array","required":false,"visibility":"user-or-llm","description":"Locations to search in (e.g., [\\"San Francisco, CA\\", \\"New York, NY\\"])"},"person_seniorities":{"type":"array","required":false,"visibility":"user-or-llm","description":"Seniority levels (one of: owner, founder, c_suite, partner, vp, head, director, manager, senior, entry, intern)"},"organization_ids":{"type":"array","required":false,"visibility":"user-or-llm","description":"Apollo organization IDs to filter by (e.g., [\\"5e66b6381e05b4008c8331b8\\"])"},"organization_names":{"type":"array","required":false,"visibility":"user-or-llm","description":"Company names to search within (legacy filter)"},"organization_locations":{"type":"array","required":false,"visibility":"user-or-llm","description":"Headquarters locations of the people\'s current employer (e.g., [\'texas\', \'tokyo\', \'spain\'])"},"q_organization_domains_list":{"type":"array","required":false,"visibility":"user-or-llm","description":"Employer domain names (e.g., [\\"apollo.io\\", \\"microsoft.com\\"]) — up to 1,000, no www. or @"},"organization_num_employees_ranges":{"type":"array","required":false,"visibility":"user-or-llm","description":"Employee count ranges for the person\'s current employer. Each entry is \\"min,max\\" (e.g., [\\"1,10\\", \\"250,500\\", \\"10000,20000\\"])"},"contact_email_status":{"type":"array","required":false,"visibility":"user-or-llm","description":"Email statuses to filter by: \\"verified\\", \\"unverified\\", \\"likely to engage\\", \\"unavailable\\""},"q_keywords":{"type":"string","required":false,"visibility":"user-or-llm","description":"Keywords to search for"},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination, default 1 (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, default 25, max 100 (e.g., 25, 50, 100)"}},"hostedApiKey":"none"},"apollo_sequence_add_contacts":{"id":"apollo_sequence_add_contacts","name":"Apollo Add Contacts to Sequence","description":"Add contacts to an Apollo sequence","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"sequence_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the sequence to add contacts to (e.g., \\"seq_abc123\\")"},"contact_ids":{"type":"array","required":false,"visibility":"user-or-llm","description":"Array of contact IDs to add to the sequence (e.g., [\\"con_abc123\\", \\"con_def456\\"]). Either contact_ids or label_names must be provided."},"label_names":{"type":"array","required":false,"visibility":"user-or-llm","description":"Array of label names to identify contacts to add to the sequence. Either contact_ids or label_names must be provided."},"send_email_from_email_account_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the email account to send from. Use the Get Email Accounts operation to look this up."},"send_email_from_email_address":{"type":"string","required":false,"visibility":"user-only","description":"Specific email address to send from within the email account."},"sequence_no_email":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts even if they have no email address"},"sequence_unverified_email":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts with unverified email addresses"},"sequence_job_change":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts who recently changed jobs"},"sequence_active_in_other_campaigns":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts active in other campaigns"},"sequence_finished_in_other_campaigns":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts who finished other campaigns"},"sequence_same_company_in_same_campaign":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts even if others from the same company are in the sequence"},"contacts_without_ownership_permission":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts without ownership permission"},"add_if_in_queue":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts even if they are in the queue"},"contact_verification_skipped":{"type":"boolean","required":false,"visibility":"user-only","description":"Skip contact verification when adding"},"user_id":{"type":"string","required":false,"visibility":"user-only","description":"ID of the user performing the action"},"status":{"type":"string","required":false,"visibility":"user-only","description":"Initial status for added contacts: \\"active\\" or \\"paused\\""},"auto_unpause_at":{"type":"string","required":false,"visibility":"user-only","description":"ISO 8601 datetime to automatically unpause contacts"}},"hostedApiKey":"none"},"apollo_sequence_search":{"id":"apollo_sequence_search","name":"Apollo Search Sequences","description":"Search for sequences/campaigns in your team\'s Apollo account (master key required)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"q_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search sequences by name (e.g., \\"Outbound Q1\\", \\"Follow-up\\")"},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, max 100 (e.g., 25, 50, 100)"}},"hostedApiKey":"none"},"apollo_task_create":{"id":"apollo_task_create","name":"Apollo Create Task","description":"Create one or more tasks in Apollo (one task per contact_id, master key required)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"user_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the Apollo user the task is assigned to"},"contact_ids":{"type":"array","required":true,"visibility":"user-or-llm","description":"Array of contact IDs. One task is created per contact."},"priority":{"type":"string","required":false,"visibility":"user-or-llm","description":"Task priority: \\"high\\", \\"medium\\", or \\"low\\" (defaults to \\"medium\\")"},"due_at":{"type":"string","required":true,"visibility":"user-or-llm","description":"Due date/time in ISO 8601 format (e.g., \\"2024-12-31T23:59:59Z\\")"},"type":{"type":"string","required":true,"visibility":"user-or-llm","description":"Task type: \\"call\\", \\"outreach_manual_email\\", \\"linkedin_step_connect\\", \\"linkedin_step_message\\", \\"linkedin_step_view_profile\\", \\"linkedin_step_interact_post\\", or \\"action_item\\""},"status":{"type":"string","required":true,"visibility":"user-or-llm","description":"Task status: \\"scheduled\\", \\"completed\\", or \\"skipped\\""},"note":{"type":"string","required":false,"visibility":"user-or-llm","description":"Free-form note providing context for the task"}},"hostedApiKey":"none"},"apollo_task_search":{"id":"apollo_task_search","name":"Apollo Search Tasks","description":"Search for tasks in Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"sort_by_field":{"type":"string","required":false,"visibility":"user-or-llm","description":"Sort field: \\"task_due_at\\" or \\"task_priority\\""},"open_factor_names":{"type":"array","required":false,"visibility":"user-or-llm","description":"Filter by status. Common values: [\\"task_types\\"] for open tasks, [\\"task_completed_at\\"] for completed tasks."},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, max 100 (e.g., 25, 50, 100)"}},"hostedApiKey":"none"},"appconfig_create_application":{"id":"appconfig_create_application","name":"AppConfig Create Application","description":"Create an application in AWS AppConfig","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the application to create"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"Description of the application"}},"hostedApiKey":"none"},"appconfig_create_configuration_profile":{"id":"appconfig_create_configuration_profile","name":"AppConfig Create Configuration Profile","description":"Create a configuration profile in an AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID to create the configuration profile in"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the configuration profile"},"locationUri":{"type":"string","required":true,"visibility":"user-or-llm","description":"Where the configuration is stored. Use \\"hosted\\" for AppConfig-hosted configurations, or an SSM/S3 URI"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"Description of the configuration profile"},"retrievalRoleArn":{"type":"string","required":false,"visibility":"user-or-llm","description":"ARN of an IAM role to retrieve the configuration (required for non-hosted URIs)"},"type":{"type":"string","required":false,"visibility":"user-or-llm","description":"Profile type: AWS.Freeform (default) or AWS.AppConfig.FeatureFlags"}},"hostedApiKey":"none"},"appconfig_create_environment":{"id":"appconfig_create_environment","name":"AppConfig Create Environment","description":"Create an environment for an AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID to create the environment in"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the environment to create"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"Description of the environment"}},"hostedApiKey":"none"},"appconfig_create_hosted_configuration_version":{"id":"appconfig_create_hosted_configuration_version","name":"AppConfig Create Hosted Configuration Version","description":"Create a new hosted configuration version for an AppConfig configuration profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to add the version to"},"content":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration content (e.g., a JSON or YAML document)"},"contentType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Content type of the configuration (e.g., application/json, text/plain)"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"Description of the configuration version"},"latestVersionNumber":{"type":"number","required":false,"visibility":"user-or-llm","description":"The version number of the latest version, used for optimistic concurrency"},"versionLabel":{"type":"string","required":false,"visibility":"user-or-llm","description":"A user-defined label for the configuration version"}},"hostedApiKey":"none"},"appconfig_delete_application":{"id":"appconfig_delete_application","name":"AppConfig Delete Application","description":"Delete an AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID to delete"}},"hostedApiKey":"none"},"appconfig_delete_configuration_profile":{"id":"appconfig_delete_configuration_profile","name":"AppConfig Delete Configuration Profile","description":"Delete an AWS AppConfig configuration profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to delete"}},"hostedApiKey":"none"},"appconfig_delete_environment":{"id":"appconfig_delete_environment","name":"AppConfig Delete Environment","description":"Delete an AWS AppConfig environment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the environment"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID to delete"}},"hostedApiKey":"none"},"appconfig_delete_hosted_configuration_version":{"id":"appconfig_delete_hosted_configuration_version","name":"AppConfig Delete Hosted Configuration Version","description":"Delete a specific hosted configuration version from an AppConfig profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID that owns the version"},"versionNumber":{"type":"number","required":true,"visibility":"user-or-llm","description":"The version number to delete"}},"hostedApiKey":"none"},"appconfig_get_application":{"id":"appconfig_get_application","name":"AppConfig Get Application","description":"Get details about a single AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID to retrieve"}},"hostedApiKey":"none"},"appconfig_get_configuration":{"id":"appconfig_get_configuration","name":"AppConfig Get Configuration","description":"Retrieve the latest deployed configuration for an AppConfig application, environment, and profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID or name to retrieve configuration for"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID or name to retrieve configuration for"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID or name to retrieve"}},"hostedApiKey":"none"},"appconfig_get_configuration_profile":{"id":"appconfig_get_configuration_profile","name":"AppConfig Get Configuration Profile","description":"Get details about a single AWS AppConfig configuration profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to retrieve"}},"hostedApiKey":"none"},"appconfig_get_deployment":{"id":"appconfig_get_deployment","name":"AppConfig Get Deployment","description":"Get details about a specific AWS AppConfig deployment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID of the deployment"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID of the deployment"},"deploymentNumber":{"type":"number","required":true,"visibility":"user-or-llm","description":"The sequence number of the deployment"}},"hostedApiKey":"none"},"appconfig_get_environment":{"id":"appconfig_get_environment","name":"AppConfig Get Environment","description":"Get details about a single AWS AppConfig environment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the environment"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID to retrieve"}},"hostedApiKey":"none"},"appconfig_get_hosted_configuration_version":{"id":"appconfig_get_hosted_configuration_version","name":"AppConfig Get Hosted Configuration Version","description":"Retrieve a specific hosted configuration version from an AppConfig profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to read the version from"},"versionNumber":{"type":"number","required":true,"visibility":"user-or-llm","description":"The version number to retrieve"}},"hostedApiKey":"none"},"appconfig_list_applications":{"id":"appconfig_list_applications","name":"AppConfig List Applications","description":"List applications in AWS AppConfig","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"maxResults":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of applications to return (1-50)"},"nextToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token from a previous response"}},"hostedApiKey":"none"},"appconfig_list_configuration_profiles":{"id":"appconfig_list_configuration_profiles","name":"AppConfig List Configuration Profiles","description":"List configuration profiles for an AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profiles"},"maxResults":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of configuration profiles to return (1-50)"},"nextToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token from a previous response"}},"hostedApiKey":"none"},"appconfig_list_deployment_strategies":{"id":"appconfig_list_deployment_strategies","name":"AppConfig List Deployment Strategies","description":"List deployment strategies available in AWS AppConfig","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"maxResults":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of deployment strategies to return (1-50)"},"nextToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token from a previous response"}},"hostedApiKey":"none"},"appconfig_list_deployments":{"id":"appconfig_list_deployments","name":"AppConfig List Deployments","description":"List deployments for an AWS AppConfig environment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID of the deployments"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID of the deployments"},"maxResults":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of deployments to return (1-50)"},"nextToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token from a previous response"}},"hostedApiKey":"none"},"appconfig_list_environments":{"id":"appconfig_list_environments","name":"AppConfig List Environments","description":"List environments for an AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the environments"},"maxResults":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of environments to return (1-50)"},"nextToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token from a previous response"}},"hostedApiKey":"none"},"appconfig_list_hosted_configuration_versions":{"id":"appconfig_list_hosted_configuration_versions","name":"AppConfig List Hosted Configuration Versions","description":"List hosted configuration versions for an AWS AppConfig configuration profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to list versions for"},"maxResults":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of versions to return (1-50)"},"nextToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token from a previous response"}},"hostedApiKey":"none"},"appconfig_start_deployment":{"id":"appconfig_start_deployment","name":"AppConfig Start Deployment","description":"Start deploying a configuration version to an AWS AppConfig environment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID to deploy in"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID to deploy to"},"deploymentStrategyId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The deployment strategy ID to use"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to deploy"},"configurationVersion":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration version to deploy"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"Description of the deployment"}},"hostedApiKey":"none"},"appconfig_stop_deployment":{"id":"appconfig_stop_deployment","name":"AppConfig Stop Deployment","description":"Stop an in-progress AWS AppConfig deployment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID of the deployment"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID of the deployment"},"deploymentNumber":{"type":"number","required":true,"visibility":"user-or-llm","description":"The sequence number of the deployment to stop"}},"hostedApiKey":"none"},"appconfig_update_application":{"id":"appconfig_update_application","name":"AppConfig Update Application","description":"Update the name or description of an AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID to update"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"New name for the application"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"New description for the application"}},"hostedApiKey":"none"},"appconfig_update_configuration_profile":{"id":"appconfig_update_configuration_profile","name":"AppConfig Update Configuration Profile","description":"Update the name, description, or retrieval role of an AppConfig configuration profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to update"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"New name for the configuration profile"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"New description for the configuration profile"},"retrievalRoleArn":{"type":"string","required":false,"visibility":"user-or-llm","description":"New ARN of the IAM role used to retrieve the configuration"}},"hostedApiKey":"none"},"appconfig_update_environment":{"id":"appconfig_update_environment","name":"AppConfig Update Environment","description":"Update the name or description of an AWS AppConfig environment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the environment"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID to update"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"New name for the environment"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"New description for the environment"}},"hostedApiKey":"none"},"arxiv_get_author_papers":{"id":"arxiv_get_author_papers","name":"ArXiv Get Author Papers","description":"Search for papers by a specific author on ArXiv.","version":"1.0.0","params":{"authorName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Author name to search for"},"maxResults":{"type":"number","required":false,"visibility":"user-only","description":"Maximum number of results to return (default: 10, max: 2000)"}},"hostedApiKey":"none"},"arxiv_get_paper":{"id":"arxiv_get_paper","name":"ArXiv Get Paper","description":"Get detailed information about a specific ArXiv paper by its ID.","version":"1.0.0","params":{"paperId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ArXiv paper ID (e.g., \\"1706.03762\\")"}},"hostedApiKey":"none"},"arxiv_search":{"id":"arxiv_search","name":"ArXiv Search","description":"Search for academic papers on ArXiv by keywords, authors, titles, or other fields.","version":"1.0.0","params":{"searchQuery":{"type":"string","required":true,"visibility":"user-or-llm","description":"The search query to execute"},"searchField":{"type":"string","required":false,"visibility":"user-only","description":"Field to search in: all, ti (title), au (author), abs (abstract), co (comment), jr (journal), cat (category), rn (report number)"},"maxResults":{"type":"number","required":false,"visibility":"user-only","description":"Maximum number of results to return (default: 10, max: 2000)"},"sortBy":{"type":"string","required":false,"visibility":"user-only","description":"Sort by: relevance, lastUpdatedDate, submittedDate (default: relevance)"},"sortOrder":{"type":"string","required":false,"visibility":"user-only","description":"Sort order: ascending, descending (default: descending)"}},"hostedApiKey":"none"},"asana_add_comment":{"id":"asana_add_comment","name":"Asana Add Comment","description":"Add a comment (story) to an Asana task","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"taskGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana task GID (numeric string)"},"text":{"type":"string","required":true,"visibility":"user-or-llm","description":"The text content of the comment"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"asana_add_followers":{"id":"asana_add_followers","name":"Asana Add Followers","description":"Add one or more followers to an Asana task","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"taskGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"GID of the Asana task (numeric string)"},"followers":{"type":"array","required":true,"visibility":"user-or-llm","description":"Array of user GIDs to add as followers to the task"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"asana_create_project":{"id":"asana_create_project","name":"Asana Create Project","description":"Create a new project in an Asana workspace","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"workspace":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana workspace GID (numeric string) where the project will be created"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the project"},"notes":{"type":"string","required":false,"visibility":"user-or-llm","description":"Notes or description for the project"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"asana_create_section":{"id":"asana_create_section","name":"Asana Create Section","description":"Create a new section in an Asana project","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"projectGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"GID of the Asana project (numeric string) to add the section to"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the section"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"asana_create_subtask":{"id":"asana_create_subtask","name":"Asana Create Subtask","description":"Create a subtask under an existing Asana task","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"taskGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"GID of the parent Asana task (numeric string)"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the subtask"},"notes":{"type":"string","required":false,"visibility":"user-or-llm","description":"Notes or description for the subtask"},"assignee":{"type":"string","required":false,"visibility":"user-or-llm","description":"User GID to assign the subtask to"},"due_on":{"type":"string","required":false,"visibility":"user-or-llm","description":"Due date in YYYY-MM-DD format"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"asana_create_task":{"id":"asana_create_task","name":"Asana Create Task","description":"Create a new task in Asana","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"workspace":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana workspace GID (numeric string) where the task will be created"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the task"},"notes":{"type":"string","required":false,"visibility":"user-or-llm","description":"Notes or description for the task"},"assignee":{"type":"string","required":false,"visibility":"user-or-llm","description":"User GID to assign the task to"},"due_on":{"type":"string","required":false,"visibility":"user-or-llm","description":"Due date in YYYY-MM-DD format"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"asana_delete_task":{"id":"asana_delete_task","name":"Asana Delete Task","description":"Delete an Asana task by its GID (moves it to the trash)","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"taskGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"GID of the Asana task to delete (numeric string)"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"asana_get_project":{"id":"asana_get_project","name":"Asana Get Project","description":"Retrieve a single Asana project by its GID","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"projectGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana project GID (numeric string) to retrieve"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"asana_get_projects":{"id":"asana_get_projects","name":"Asana Get Projects","description":"Retrieve all projects from an Asana workspace","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"workspace":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana workspace GID (numeric string) to retrieve projects from"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"asana_get_task":{"id":"asana_get_task","name":"Asana Get Task","description":"Retrieve a single task by GID or get multiple tasks with filters","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"taskGid":{"type":"string","required":false,"visibility":"user-or-llm","description":"The globally unique identifier (GID) of the task. If not provided, will get multiple tasks."},"workspace":{"type":"string","required":false,"visibility":"user-or-llm","description":"Asana workspace GID (numeric string) to filter tasks (required when not using taskGid)"},"project":{"type":"string","required":false,"visibility":"user-or-llm","description":"Asana project GID (numeric string) to filter tasks"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of tasks to return (default: 50)"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"asana_list_sections":{"id":"asana_list_sections","name":"Asana List Sections","description":"List all sections in an Asana project","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"projectGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"GID of the Asana project (numeric string) to list sections from"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"asana_list_workspaces":{"id":"asana_list_workspaces","name":"Asana List Workspaces","description":"List all Asana workspaces and organizations the authenticated user belongs to","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"asana_search_tasks":{"id":"asana_search_tasks","name":"Asana Search Tasks","description":"Search for tasks in an Asana workspace","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"workspace":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana workspace GID (numeric string) to search tasks in"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Text to search for in task names"},"assignee":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter tasks by assignee user GID"},"projects":{"type":"array","required":false,"visibility":"user-or-llm","description":"Array of Asana project GIDs (numeric strings) to filter tasks by"},"completed":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Filter by completion status"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"asana_update_task":{"id":"asana_update_task","name":"Asana Update Task","description":"Update an existing task in Asana","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"taskGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana task GID (numeric string) of the task to update"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Updated name for the task"},"notes":{"type":"string","required":false,"visibility":"user-or-llm","description":"Updated notes or description for the task"},"assignee":{"type":"string","required":false,"visibility":"user-or-llm","description":"Updated assignee user GID"},"completed":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Mark task as completed or not completed"},"due_on":{"type":"string","required":false,"visibility":"user-or-llm","description":"Updated due date in YYYY-MM-DD format"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"ashby_add_candidate_tag":{"id":"ashby_add_candidate_tag","name":"Ashby Add Candidate Tag","description":"Adds a tag to a candidate in Ashby and returns the updated candidate.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ashby API Key"},"onBehalfOfUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls"},"candidateId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the candidate to add the tag to"},"tagId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the tag to add"}},"hostedApiKey":"none"},"ashby_anonymize_candidate":{"id":"ashby_anonymize_candidate","name":"Ashby Anonymize Candidate","description":"Remove personally identifiable information from an Ashby candidate while retaining the candidate record and its applications. This operation does not delete the candidate. Requires the candidatesWrite permission.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ashby API Key"},"onBehalfOfUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls"},"candidateId":{"type":"string","required":true,"visibility":"user-or-llm","description":"UUID of the candidate to anonymize"}},"hostedApiKey":"none"},"ashby_change_application_source":{"id":"ashby_change_application_source","name":"Ashby Change Application Source","description":"Changes the source attributed to an existing application, so programmatically created applications report correctly on the recruiting side. Requires the candidatesWrite permission.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ashby API Key"},"onBehalfOfUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"UUID of the application whose source should change"},"sourceId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the source to attribute the application to, as returned by List Sources. Omit only when unsetSource is true."},"unsetSource":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Set true to deliberately clear the application source. Required to unset, so that a missing or empty sourceId cannot wipe attribution by accident."}},"hostedApiKey":"none"},"ashby_change_application_stage":{"id":"ashby_change_application_stage","name":"Ashby Change Application Stage","description":"Moves an application to a different interview stage. Requires an archive reason when moving to an Archived stage.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ashby API Key"},"onBehalfOfUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the application to update the stage of"},"interviewStageId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the interview stage to move the application to"},"archiveReasonId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Archive reason UUID. Required when moving to an Archived stage, ignored otherwise"},"archiveEmail":{"type":"json","required":false,"visibility":"user-or-llm","description":"Archive email configuration with communicationTemplateId and optional sendAt ISO 8601 timestamp. Pass null or omit to send no archive email."}},"hostedApiKey":"none"},"ashby_create_application":{"id":"ashby_create_application","name":"Ashby Create Application","description":"Creates a new application for a candidate on a job. Optionally specify interview plan, stage, source, and credited user.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ashby API Key"},"onBehalfOfUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls"},"candidateId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the candidate to consider for the job"},"jobId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the job to consider the candidate for"},"interviewPlanId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the interview plan to use (defaults to the job default plan)"},"interviewStageId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the interview stage to place the application in, or FirstPreInterviewScreen (defaults to the first Lead stage)"},"sourceId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the source to set on the application"},"creditedToUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the user the application is credited to"},"createdAt":{"type":"string","required":false,"visibility":"user-or-llm","description":"ISO 8601 timestamp to set as the application creation date (defaults to now)"},"applicationHistory":{"type":"json","required":false,"visibility":"user-or-llm","description":"Optional documented application history entries to create with the application"}},"hostedApiKey":"none"},"ashby_create_candidate":{"id":"ashby_create_candidate","name":"Ashby Create Candidate","description":"Creates a new candidate record in Ashby.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ashby API Key"},"onBehalfOfUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"The candidate full name"},"email":{"type":"string","required":false,"visibility":"user-or-llm","description":"Primary email address for the candidate"},"phoneNumber":{"type":"string","required":false,"visibility":"user-or-llm","description":"Primary phone number for the candidate"},"linkedInUrl":{"type":"string","required":false,"visibility":"user-or-llm","description":"LinkedIn profile URL"},"githubUrl":{"type":"string","required":false,"visibility":"user-or-llm","description":"GitHub profile URL"},"website":{"type":"string","required":false,"visibility":"user-or-llm","description":"Personal website URL"},"sourceId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the source to attribute the candidate to"},"creditedToUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the Ashby user to credit with sourcing this candidate"},"createdAt":{"type":"string","required":false,"visibility":"user-or-llm","description":"Backdated creation timestamp in ISO 8601 (e.g. 2024-01-01T00:00:00Z). Defaults to now."},"alternateEmailAddresses":{"type":"json","required":false,"visibility":"user-or-llm","description":"Array of additional email address strings to add to the candidate, e.g. [\\"a@x.com\\",\\"b@y.com\\"]"},"location":{"type":"json","required":false,"visibility":"user-or-llm","description":"Candidate location object with optional city, region, and country"}},"hostedApiKey":"none"},"ashby_create_note":{"id":"ashby_create_note","name":"Ashby Create Note","description":"Creates a note on a candidate in Ashby. Supports plain text and HTML content (bold, italic, underline, links, lists, code).","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ashby API Key"},"onBehalfOfUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls"},"candidateId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the candidate to add the note to"},"note":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note content. If noteType is text/html, supports: , , , ,