diff --git a/.github/DEVELOPING_V3.md b/.github/DEVELOPING_V3.md
index 09f57dd588f..399cd87d1da 100644
--- a/.github/DEVELOPING_V3.md
+++ b/.github/DEVELOPING_V3.md
@@ -135,6 +135,7 @@ of the breaking removal on `3.x`.
`master` is dropped as empty.
3. Conflicts confined to **mechanical files** — tool-generated content with a deterministic
resolution (`pnpm-lock.yaml`, `packages/frontend/editor-ui/data/node-popularity.json`,
+ `packages/@n8n/instance-ai/src/tools/nodes/credential-setupability.json`, and
`.github/test-metrics/e2e-impact-map.json`) — are **auto-resolved during the replay**,
exactly as a human resolver would: the lockfile is regenerated with
`pnpm install --lockfile-only` (pnpm merges its own conflict markers), bot-maintained data
diff --git a/.github/scripts/fetch-credential-setupability.mjs b/.github/scripts/fetch-credential-setupability.mjs
new file mode 100644
index 00000000000..25e09839eef
--- /dev/null
+++ b/.github/scripts/fetch-credential-setupability.mjs
@@ -0,0 +1,47 @@
+#!/usr/bin/env node
+import { writeFile } from 'node:fs/promises';
+import { dirname, resolve } from 'node:path';
+import { fileURLToPath, pathToFileURL } from 'node:url';
+
+const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
+const outputFile = resolve(
+ repositoryRoot,
+ 'packages/@n8n/instance-ai/src/tools/nodes/credential-setupability.json',
+);
+const endpoint = process.env.N8N_CREDENTIAL_SETUPABILITY_ENDPOINT;
+
+const roundSetupability = (value) => (value === null ? null : Math.round(value * 20) / 20);
+export const roundPopularity = (value) => (value === null ? null : Math.round(value * 10) / 10);
+
+async function main() {
+ if (!endpoint) {
+ throw new Error('N8N_CREDENTIAL_SETUPABILITY_ENDPOINT is required.');
+ }
+
+ console.log('Fetching credential setupability data.');
+ const response = await fetch(endpoint, { signal: AbortSignal.timeout(10_000) });
+ if (!response.ok) {
+ throw new Error(`Credential setupability endpoint returned HTTP ${response.status}.`);
+ }
+
+ const data = await response.json();
+ if (!Array.isArray(data) || data.length === 0) {
+ throw new Error('Credential setupability endpoint returned no data.');
+ }
+
+ const metrics = data.map(({ id, setupability, popularity }) => ({
+ id,
+ setupability: roundSetupability(setupability),
+ popularity: roundPopularity(popularity),
+ }));
+
+ await writeFile(outputFile, `${JSON.stringify(metrics, null, '\t')}\n`, 'utf8');
+ console.log(`Saved credential setupability data for ${metrics.length} credential types.`);
+}
+
+if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
+ main().catch((error) => {
+ console.error(error instanceof Error ? error.message : String(error));
+ process.exitCode = 1;
+ });
+}
diff --git a/.github/scripts/fetch-credential-setupability.test.mjs b/.github/scripts/fetch-credential-setupability.test.mjs
new file mode 100644
index 00000000000..6b1065d76d5
--- /dev/null
+++ b/.github/scripts/fetch-credential-setupability.test.mjs
@@ -0,0 +1,14 @@
+import assert from 'node:assert/strict';
+import { describe, it } from 'node:test';
+
+import { roundPopularity } from './fetch-credential-setupability.mjs';
+
+describe('roundPopularity', () => {
+ it('preserves a missing value', () => {
+ assert.equal(roundPopularity(null), null);
+ });
+
+ it('rounds a value to one decimal place', () => {
+ assert.equal(roundPopularity(0.26), 0.3);
+ });
+});
diff --git a/.github/scripts/sync-master-to-3x.mjs b/.github/scripts/sync-master-to-3x.mjs
index 8418133b757..3d6013a5992 100644
--- a/.github/scripts/sync-master-to-3x.mjs
+++ b/.github/scripts/sync-master-to-3x.mjs
@@ -97,6 +97,7 @@ export const LOCKFILE = 'pnpm-lock.yaml';
export const MECHANICAL_PATHS = {
[LOCKFILE]: 'pnpm-regen',
'packages/frontend/editor-ui/data/node-popularity.json': 'take-master',
+ 'packages/@n8n/instance-ai/src/tools/nodes/credential-setupability.json': 'take-master',
'.github/test-metrics/e2e-impact-map.json': 'take-master',
};
diff --git a/.github/scripts/sync-master-to-3x.test.mjs b/.github/scripts/sync-master-to-3x.test.mjs
index 0a807f0e0c7..cf41390943a 100644
--- a/.github/scripts/sync-master-to-3x.test.mjs
+++ b/.github/scripts/sync-master-to-3x.test.mjs
@@ -62,6 +62,7 @@ const PRE_HEAD = 'PREHEAD';
const MASTER = 'MASTERSHA';
const MERGE_TREE = 'MERGETREEOID';
const POPULARITY = 'packages/frontend/editor-ui/data/node-popularity.json';
+const SETUPABILITY = 'packages/@n8n/instance-ai/src/tools/nodes/credential-setupability.json';
const isRebase = (a) => a[0] === 'rebase' && a[1] !== '--abort';
const favouringOwnSide = (a) => a[0] === 'rebase' && a.includes('-X') && a.includes('theirs');
@@ -138,9 +139,14 @@ test('classifyPaths and blocksLockfileRegen split mechanical from code conflicts
const { mechanical, code } = classifyPaths([
LOCKFILE,
'packages/cli/x.ts',
+ SETUPABILITY,
+ '.github/test-metrics/e2e-impact-map.json',
+ ]);
+ assert.deepEqual(mechanical, [
+ LOCKFILE,
+ SETUPABILITY,
'.github/test-metrics/e2e-impact-map.json',
]);
- assert.deepEqual(mechanical, [LOCKFILE, '.github/test-metrics/e2e-impact-map.json']);
assert.deepEqual(code, ['packages/cli/x.ts']);
assert.equal(blocksLockfileRegen(['packages/cli/package.json']), true);
diff --git a/.github/workflows/util-update-credential-setupability.yml b/.github/workflows/util-update-credential-setupability.yml
new file mode 100644
index 00000000000..bc7adb5813a
--- /dev/null
+++ b/.github/workflows/util-update-credential-setupability.yml
@@ -0,0 +1,102 @@
+name: 'Util: Update Credential Setupability'
+
+on:
+ schedule:
+ # Run every Monday at 00:30 UTC, after the node-popularity refresh starts.
+ - cron: '30 0 * * 1'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: update-credential-setupability
+ cancel-in-progress: false
+
+jobs:
+ update-setupability:
+ if: >-
+ github.repository == 'n8n-io/n8n' &&
+ vars.N8N_CREDENTIAL_SETUPABILITY_ENDPOINT != ''
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ outputs:
+ pull-request-number: ${{ steps.create-pr.outputs.pull-request-number }}
+ steps:
+ - name: Generate GitHub App Token
+ id: generate-token
+ uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
+ with:
+ app-id: ${{ secrets.N8N_ASSISTANT_APP_ID }}
+ private-key: ${{ secrets.N8N_ASSISTANT_PRIVATE_KEY }}
+ permission-contents: write
+ permission-pull-requests: write
+
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+
+ - name: Setup Node.js and dependencies
+ uses: ./.github/actions/setup-nodejs
+ with:
+ build-command: ''
+
+ - name: Fetch credential setupability data
+ run: node .github/scripts/fetch-credential-setupability.mjs
+ env:
+ N8N_CREDENTIAL_SETUPABILITY_ENDPOINT: ${{ vars.N8N_CREDENTIAL_SETUPABILITY_ENDPOINT }}
+
+ - name: Format generated file
+ run: >-
+ pnpm biome format --write
+ packages/@n8n/instance-ai/src/tools/nodes/credential-setupability.json
+
+ - name: Check for changes
+ id: check-changes
+ run: |
+ if [ -z "$(git status --porcelain -- packages/@n8n/instance-ai/src/tools/nodes/credential-setupability.json)" ]; then
+ echo "No changes to credential setupability data"
+ echo "has_changes=false" >> "$GITHUB_OUTPUT"
+ else
+ echo "Credential setupability data has changed"
+ echo "has_changes=true" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Create pull request
+ if: steps.check-changes.outputs.has_changes == 'true'
+ id: create-pr
+ uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
+ with:
+ token: ${{ steps.generate-token.outputs.token }}
+ add-paths: packages/@n8n/instance-ai/src/tools/nodes/credential-setupability.json
+ commit-message: 'chore: Update credential setupability data'
+ labels: 'automation:scheduled-update'
+ title: 'chore: Update credential setupability data'
+ body: |
+ This automated PR updates the credential setupability data used by Instance AI node discovery.
+
+ The data is fetched weekly from the internal telemetry endpoint. It contains rounded credential-type setup completion and popularity scores.
+
+ _Generated by the weekly credential setupability update workflow._
+ branch: update-credential-setupability
+ base: master
+ delete-branch: true
+ author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
+ committer: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
+
+ approve-and-automerge:
+ needs: [update-setupability]
+ if: >-
+ needs.update-setupability.outputs.pull-request-number != '' &&
+ github.repository == 'n8n-io/n8n'
+ permissions:
+ contents: write
+ pull-requests: write
+ # The callee's app credentials live on the release environment and cannot
+ # be forwarded as named secrets by the caller.
+ uses: ./.github/workflows/util-approve-and-set-automerge.yml # zizmor: ignore[secrets-inherit]
+ secrets: inherit
+ with:
+ pull-request-number: ${{ needs.update-setupability.outputs.pull-request-number }}
diff --git a/packages/@n8n/instance-ai/evaluations/__tests__/conversation-text.test.ts b/packages/@n8n/instance-ai/evaluations/__tests__/conversation-text.test.ts
index 5c27f73e578..4c716321025 100644
--- a/packages/@n8n/instance-ai/evaluations/__tests__/conversation-text.test.ts
+++ b/packages/@n8n/instance-ai/evaluations/__tests__/conversation-text.test.ts
@@ -237,6 +237,30 @@ describe('transcriptAsText', () => {
expect(text).toContain('prompt: Here is the plan, approve?');
expect(text).toContain('user feedback: No — use a Webhook trigger, not a Schedule');
});
+
+ it('surfaces ask-user question types for process expectations', () => {
+ const transcript: TranscriptTurn[] = [
+ {
+ steps: [
+ {
+ kind: 'ask-user',
+ questions: [
+ {
+ id: 'service',
+ question: 'Which service?',
+ type: 'single',
+ options: ['RocketChat', 'Zulip'],
+ },
+ ],
+ },
+ ],
+ },
+ ];
+
+ expect(transcriptAsText(transcript)).toContain(
+ 'Q (single): Which service? [RocketChat / Zulip]',
+ );
+ });
});
describe('perTurnToolCallCounts', () => {
diff --git a/packages/@n8n/instance-ai/evaluations/__tests__/transcript-from-events.test.ts b/packages/@n8n/instance-ai/evaluations/__tests__/transcript-from-events.test.ts
index a8f295a3da6..468ec16af9d 100644
--- a/packages/@n8n/instance-ai/evaluations/__tests__/transcript-from-events.test.ts
+++ b/packages/@n8n/instance-ai/evaluations/__tests__/transcript-from-events.test.ts
@@ -206,7 +206,9 @@ describe('buildTranscriptFromEvents', () => {
});
describe('ask-user routing', () => {
- const questions = [{ id: 'q1', question: 'Which channels?' }];
+ const questions = [
+ { id: 'q1', question: 'Which channel?', type: 'single', options: ['Slack', 'Teams'] },
+ ];
it('renders ask-user from confirmation-request and skips the tool-call twin', () => {
const turns = buildTranscriptFromEvents({
@@ -222,7 +224,7 @@ describe('buildTranscriptFromEvents', () => {
'r1',
{
kind: 'questions' as const,
- answers: [{ questionId: 'q1', selectedOptions: ['#general'] }],
+ answers: [{ questionId: 'q1', selectedOptions: ['Teams'] }],
},
],
]),
@@ -231,8 +233,10 @@ describe('buildTranscriptFromEvents', () => {
expect(interactions).toHaveLength(1);
expect(interactions[0]).toMatchObject({
kind: 'ask-user',
- questions: [{ id: 'q1', question: 'Which channels?' }],
- answers: [{ questionId: 'q1', selectedOptions: ['#general'] }],
+ questions: [
+ { id: 'q1', question: 'Which channel?', type: 'single', options: ['Slack', 'Teams'] },
+ ],
+ answers: [{ questionId: 'q1', selectedOptions: ['Teams'] }],
});
});
diff --git a/packages/@n8n/instance-ai/evaluations/__tests__/workflow-report.test.ts b/packages/@n8n/instance-ai/evaluations/__tests__/workflow-report.test.ts
index 92dd066f079..2364a58506f 100644
--- a/packages/@n8n/instance-ai/evaluations/__tests__/workflow-report.test.ts
+++ b/packages/@n8n/instance-ai/evaluations/__tests__/workflow-report.test.ts
@@ -196,7 +196,14 @@ describe('transcript rendering', () => {
steps: [
{
kind: 'ask-user',
- questions: [{ id: 'q1', question: 'Which channel?', options: ['#a', '#b'] }],
+ questions: [
+ {
+ id: 'q1',
+ question: 'Which channel?',
+ type: 'single',
+ options: ['#a', '#b'],
+ },
+ ],
answers: [{ questionId: 'q1', selectedOptions: [], skipped: true }],
},
],
@@ -206,6 +213,7 @@ describe('transcript rendering', () => {
const html = generateWorkflowReport([result]);
expect(html).toContain('👤 (skipped)');
expect(html).toContain('ask-user (with answers)');
+ expect(html).toContain('single');
});
});
diff --git a/packages/@n8n/instance-ai/evaluations/outcome/transcript-from-events.ts b/packages/@n8n/instance-ai/evaluations/outcome/transcript-from-events.ts
index 21bebda2cef..b7972de4ef2 100644
--- a/packages/@n8n/instance-ai/evaluations/outcome/transcript-from-events.ts
+++ b/packages/@n8n/instance-ai/evaluations/outcome/transcript-from-events.ts
@@ -446,10 +446,14 @@ export function extractAskUserQuestions(raw: unknown[]): AskUserQuestion[] {
if (!isRecord(item)) continue;
const id = typeof item.id === 'string' ? item.id : '';
const question = typeof item.question === 'string' ? item.question : '';
+ const type =
+ item.type === 'single' || item.type === 'multi' || item.type === 'text'
+ ? item.type
+ : undefined;
const options = Array.isArray(item.options)
? item.options.filter((o): o is string => typeof o === 'string')
: undefined;
- if (id || question) questions.push({ id, question, options });
+ if (id || question) questions.push({ id, question, type, options });
}
return questions;
}
diff --git a/packages/@n8n/instance-ai/evaluations/report/workflow-report.ts b/packages/@n8n/instance-ai/evaluations/report/workflow-report.ts
index 7465c0fa1ab..f91382cbe16 100644
--- a/packages/@n8n/instance-ai/evaluations/report/workflow-report.ts
+++ b/packages/@n8n/instance-ai/evaluations/report/workflow-report.ts
@@ -1018,6 +1018,7 @@ function renderInteraction(interaction: ToolInteraction): string | null {
}
const lines = interaction.questions
.map((q) => {
+ const type = q.type ? ` ${escapeHtml(q.type)}` : '';
const opts =
q.options && q.options.length > 0
? ` (${q.options.map((o) => escapeHtml(o)).join(' / ')})`
@@ -1026,7 +1027,7 @@ function renderInteraction(interaction: ToolInteraction): string | null {
const answerHtml = answer
? `
👤 ${escapeHtml(answer)}
`
: '';
- return `${escapeHtml(q.question)}${opts}${answerHtml}`;
+ return `${escapeHtml(q.question)}${type}${opts}${answerHtml}`;
})
.join('');
const summary =
diff --git a/packages/@n8n/instance-ai/evaluations/types.ts b/packages/@n8n/instance-ai/evaluations/types.ts
index caba08ceb29..30a5be38405 100644
--- a/packages/@n8n/instance-ai/evaluations/types.ts
+++ b/packages/@n8n/instance-ai/evaluations/types.ts
@@ -448,6 +448,7 @@ export interface PlanTask {
export interface AskUserQuestion {
id: string;
question: string;
+ type?: 'single' | 'multi' | 'text';
options?: string[];
}
diff --git a/packages/@n8n/instance-ai/evaluations/utils/conversation-text.ts b/packages/@n8n/instance-ai/evaluations/utils/conversation-text.ts
index 0839b130bb3..287b0e78600 100644
--- a/packages/@n8n/instance-ai/evaluations/utils/conversation-text.ts
+++ b/packages/@n8n/instance-ai/evaluations/utils/conversation-text.ts
@@ -235,9 +235,10 @@ function describeInteraction(interaction: ToolInteraction): string | null {
}
const qs = interaction.questions
.map((q) => {
+ const type = q.type ? ` (${q.type})` : '';
const opts = q.options && q.options.length > 0 ? ` [${q.options.join(' / ')}]` : '';
const answer = answerByQId.get(q.id);
- return `Q: ${q.question}${opts}${answer ? ` -> A: ${answer}` : ''}`;
+ return `Q${type}: ${q.question}${opts}${answer ? ` -> A: ${answer}` : ''}`;
})
.join(' | ');
return `Asked user: ${qs}`;
diff --git a/packages/@n8n/instance-ai/skills/workflow-builder/SKILL.md b/packages/@n8n/instance-ai/skills/workflow-builder/SKILL.md
index 79254b563ba..67f30263bb0 100644
--- a/packages/@n8n/instance-ai/skills/workflow-builder/SKILL.md
+++ b/packages/@n8n/instance-ai/skills/workflow-builder/SKILL.md
@@ -394,6 +394,41 @@ decision after testing.
- Always declare `output` on nodes that use unresolved credentials when mock
data is needed for verification.
+## Credential Setup Preference
+
+Discovery results can include a `setupPreference` array. Each entry has:
+
+- `type`, the credential type
+- `setupCompletionPercent`, a percentage from 0 to 100 rounded to the nearest
+ 5 percentage points, or `null`
+- `popularityScore`, a relative adoption score from 0 to 1 rounded to one
+ decimal place, or `null`
+
+Setup completion measures completion of an Instance AI setup step containing
+the credential; it is not an activation or validity rate. For either metric,
+`null` means there was not enough data. Popularity is relative recent adoption,
+not a percentage. Treat both as coarse signals and ignore small differences.
+
+When choosing a service:
+
+1. Honor explicit intent and existing workflow choices.
+2. Prefer a semantically suitable service with a usable existing credential,
+ then apply the existing Gateway credits rules.
+3. Compare setup preference only among the remaining semantically
+ interchangeable candidates. Before deciding, inspect discovery results for
+ every candidate the user named.
+
+- When setup completion and popularity clearly support one candidate, choose it
+ and continue without asking.
+- When the signals are close or conflict and the user has not delegated the
+ choice, ask exactly one `single` question. If skipped, choose a sensible default.
+- When the user explicitly asks you to choose, make a sensible choice and
+ continue without asking.
+
+Use judgment instead of calculating a combined score or applying a fixed
+threshold. Never let this metadata override stronger semantic relevance or use
+it to choose between authentication methods for the same service.
+
## Gateway credits Preference
"Gateway credits" is the user-facing name of n8n's managed credential
diff --git a/packages/@n8n/instance-ai/src/tools/__tests__/nodes.tool.test.ts b/packages/@n8n/instance-ai/src/tools/__tests__/nodes.tool.test.ts
index 8166425ca08..f9eb7270543 100644
--- a/packages/@n8n/instance-ai/src/tools/__tests__/nodes.tool.test.ts
+++ b/packages/@n8n/instance-ai/src/tools/__tests__/nodes.tool.test.ts
@@ -1,7 +1,13 @@
+import {
+ NodeSearchEngine,
+ suggestedNodesData,
+ type SearchableNodeType,
+} from '@n8n/ai-utilities/node-catalog';
import type { Mock } from 'vitest';
import { executeTool } from '../../__tests__/tool-test-utils';
import type { InstanceAiContext } from '../../types';
+import { addSetupPreference } from '../nodes/setup-preference';
import { createNodesTool } from '../nodes.tool';
function createMockContext(overrides: Partial = {}): InstanceAiContext {
@@ -181,6 +187,10 @@ describe('nodes tool', () => {
];
const context = createMockContext();
(context.nodeService.listSearchable as Mock).mockResolvedValue(searchableNodes);
+ (context.nodeService.getDescription as Mock).mockResolvedValue({
+ properties: [{ type: 'credentialsSelect' }],
+ credentials: [{ name: 'gmailOAuth2' }],
+ });
const tool = createNodesTool(context, 'full');
const first = await executeTool(
@@ -203,6 +213,8 @@ describe('nodes tool', () => {
totalResults: 1,
results: [expect.objectContaining({ name: 'n8n-nodes-base.httpRequest' })],
});
+ expect(context.nodeService.getDescription).toHaveBeenCalledTimes(2);
+ expect(first).not.toHaveProperty('results.0.setupPreference');
});
it('should search nodes by connection type and enrich results with discriminators', async () => {
@@ -355,6 +367,105 @@ describe('nodes tool', () => {
});
});
+ describe('setup preference', () => {
+ it('should expose credential preferences without changing existing data or order', async () => {
+ const expectedTelegram = addSetupPreference({}, ['telegramApi']).setupPreference;
+ const expectedGmail = addSetupPreference({}, ['gmailOAuth2', 'googleApi']).setupPreference;
+ const credentialsByNode = new Map([
+ ['n8n-nodes-base.gmail', ['gmailOAuth2', 'googleApi']],
+ ['n8n-nodes-base.httpRequest', ['gmailOAuth2']],
+ ['n8n-nodes-base.telegram', ['telegramApi']],
+ ]);
+ const searchableNodes = [
+ {
+ name: 'n8n-nodes-base.telegram',
+ displayName: 'Telegram',
+ description: 'Send a notification message',
+ inputs: ['main'],
+ outputs: ['main'],
+ version: 1,
+ aiGateway: { supported: true, minVersion: 1 },
+ },
+ {
+ name: 'n8n-nodes-base.unknownSetupability',
+ displayName: 'Unknown Setupability',
+ description: 'Send a notification message',
+ inputs: ['main'],
+ outputs: ['main'],
+ version: 1,
+ },
+ ] satisfies SearchableNodeType[];
+ const expectedSearchResults = new NodeSearchEngine(searchableNodes).searchByName(
+ 'message',
+ 5,
+ );
+ const context = createMockContext();
+ (context.nodeService.listSearchable as Mock).mockResolvedValue(searchableNodes);
+ (context.nodeService.getDescription as Mock).mockImplementation((nodeType: string) => ({
+ properties:
+ nodeType === 'n8n-nodes-base.httpRequest' ? [{ type: 'credentialsSelect' }] : [],
+ credentials: (credentialsByNode.get(nodeType) ?? []).map((name) => ({ name })),
+ }));
+ context.nodeService.listDiscriminators = vi.fn().mockResolvedValue({
+ resource: ['message'],
+ });
+
+ const tool = createNodesTool(context, 'full');
+ const searchResult = await executeTool<{
+ results: Array<{
+ name: string;
+ score: number;
+ note?: string;
+ aiGateway?: unknown;
+ discriminators?: unknown;
+ setupPreference?: unknown;
+ }>;
+ }>(tool, { action: 'search', query: 'message', limit: 5 } as never, {} as never);
+ const suggestedResult = await executeTool<{
+ results: Array<{
+ suggestedNodes: Array<{
+ name: string;
+ note?: string;
+ setupPreference?: unknown;
+ }>;
+ }>;
+ }>(tool, { action: 'suggested', categories: ['notification'] } as never, {} as never);
+
+ expect(searchResult.results.map(({ name, score }) => ({ name, score }))).toEqual(
+ expectedSearchResults.map(({ name, score }) => ({ name, score })),
+ );
+ const searchTelegram = searchResult.results.find(
+ (node) => node.name === 'n8n-nodes-base.telegram',
+ );
+ const searchUnknown = searchResult.results.find(
+ (node) => node.name === 'n8n-nodes-base.unknownSetupability',
+ );
+ const notificationNodes = suggestedResult.results[0]?.suggestedNodes;
+ const suggestedTelegram = notificationNodes?.find(
+ (node) => node.name === 'n8n-nodes-base.telegram',
+ );
+
+ expect(searchTelegram).toMatchObject({
+ aiGateway: { supported: true, minVersion: 1 },
+ discriminators: { resource: ['message'] },
+ setupPreference: expectedTelegram,
+ });
+ expect(suggestedTelegram?.setupPreference).toEqual(searchTelegram?.setupPreference);
+ expect(searchUnknown).not.toHaveProperty('setupPreference');
+ expect(notificationNodes?.map(({ name }) => name)).toEqual(
+ suggestedNodesData.notification.nodes.map(({ name }) => name),
+ );
+ expect(notificationNodes?.find((node) => node.name === 'n8n-nodes-base.gmail')).toEqual({
+ name: 'n8n-nodes-base.gmail',
+ note: "Default to this because it's easy for users to setup",
+ setupPreference: expectedGmail,
+ });
+ expect(
+ notificationNodes?.find((node) => node.name === 'n8n-nodes-base.httpRequest'),
+ ).not.toHaveProperty('setupPreference');
+ });
+ });
+
describe('explore-resources action', () => {
it('should return error when exploreResources is not available', async () => {
const context = createMockContext();
diff --git a/packages/@n8n/instance-ai/src/tools/nodes.tool.ts b/packages/@n8n/instance-ai/src/tools/nodes.tool.ts
index 99b03736fd6..7b206682511 100644
--- a/packages/@n8n/instance-ai/src/tools/nodes.tool.ts
+++ b/packages/@n8n/instance-ai/src/tools/nodes.tool.ts
@@ -7,6 +7,7 @@ import {
NodeSearchEngine,
categoryList,
suggestedNodesData,
+ type CategorySuggestedNode,
type SearchableNodeType,
} from '@n8n/ai-utilities/node-catalog';
import { z } from 'zod';
@@ -14,6 +15,7 @@ import { z } from 'zod';
import { sanitizeInputSchema } from '../agent/sanitize-mcp-schemas';
import type { InstanceAiContext } from '../types';
import { pickPreferredChatModelNode } from './nodes/preferred-chat-model';
+import { addSetupPreference, type NodeWithSetupPreference } from './nodes/setup-preference';
import { buildCredentialMap } from './workflows/resolve-credentials';
// ── Action schemas ──────────────────────────────────────────────────────────
@@ -145,6 +147,21 @@ interface SearchEngineCache {
engine?: NodeSearchEngine;
}
+async function enrichWithSetupPreference(
+ context: InstanceAiContext,
+ node: T,
+ version?: number,
+): Promise> {
+ try {
+ const description = await context.nodeService.getDescription(node.name, version);
+ if (description.properties.some(({ type }) => type === 'credentialsSelect')) return node;
+
+ return addSetupPreference(node, description.credentials?.map(({ name }) => name) ?? []);
+ } catch {
+ return node;
+ }
+}
+
// ── Handlers ────────────────────────────────────────────────────────────────
async function handleList(
@@ -181,13 +198,15 @@ async function handleSearch(
return { results: [], totalResults: 0 };
}
- // Enrich results with discriminator info (resources/operations) when available
+ // Enrich results with discriminator and credential setup metadata when available.
const enriched = await Promise.all(
results.map(async (r) => {
- if (!context.nodeService.listDiscriminators) return r;
- const disc = await context.nodeService.listDiscriminators(r.name);
- if (!disc) return r;
- return { ...r, discriminators: disc };
+ const [node, discriminators] = await Promise.all([
+ enrichWithSetupPreference(context, r, r.version),
+ context.nodeService.listDiscriminators?.(r.name) ?? Promise.resolve(null),
+ ]);
+
+ return discriminators ? { ...node, discriminators } : node;
}),
);
@@ -323,24 +342,29 @@ async function handleTypeDefinition(
return await resolveNodeTypeDefinitions(context, parsed.data.nodeTypes);
}
-// eslint-disable-next-line @typescript-eslint/require-await
-async function handleSuggested(input: Extract) {
+async function handleSuggested(
+ context: InstanceAiContext,
+ input: Extract,
+) {
const results: Array<{
category: string;
description: string;
patternHint: string;
- suggestedNodes: Array<{ name: string; note?: string }>;
+ suggestedNodes: Array>;
}> = [];
const unknownCategories: string[] = [];
for (const cat of input.categories) {
const data = suggestedNodesData[cat];
if (data) {
+ const suggestedNodes = await Promise.all(
+ data.nodes.map(async (node) => await enrichWithSetupPreference(context, node)),
+ );
results.push({
category: cat,
description: data.description,
patternHint: data.patternHint,
- suggestedNodes: data.nodes,
+ suggestedNodes,
});
} else {
unknownCategories.push(cat);
@@ -447,7 +471,7 @@ export function createNodesTool(
case 'type-definition':
return await handleTypeDefinition(context, input);
case 'suggested':
- return await handleSuggested(input);
+ return await handleSuggested(context, input);
case 'explore-resources':
return await handleExploreResources(context, input);
}
diff --git a/packages/@n8n/instance-ai/src/tools/nodes/__tests__/setup-preference-null.test.ts b/packages/@n8n/instance-ai/src/tools/nodes/__tests__/setup-preference-null.test.ts
new file mode 100644
index 00000000000..4b89da13441
--- /dev/null
+++ b/packages/@n8n/instance-ai/src/tools/nodes/__tests__/setup-preference-null.test.ts
@@ -0,0 +1,27 @@
+import { addSetupPreference } from '../setup-preference';
+
+vi.mock('../credential-setupability.json', () => ({
+ default: [
+ { id: 'knownCredential', setupability: 0.42, popularity: 0.26 },
+ { id: 'unknownCredential', setupability: null, popularity: null },
+ ],
+}));
+
+describe('credential setup preference with missing data', () => {
+ it('preserves missing popularity and rounds known popularity', () => {
+ expect(addSetupPreference({}, ['knownCredential', 'unknownCredential'])).toEqual({
+ setupPreference: [
+ {
+ type: 'knownCredential',
+ setupCompletionPercent: 42,
+ popularityScore: 0.3,
+ },
+ {
+ type: 'unknownCredential',
+ setupCompletionPercent: null,
+ popularityScore: null,
+ },
+ ],
+ });
+ });
+});
diff --git a/packages/@n8n/instance-ai/src/tools/nodes/__tests__/setup-preference.test.ts b/packages/@n8n/instance-ai/src/tools/nodes/__tests__/setup-preference.test.ts
new file mode 100644
index 00000000000..00ccb3fb930
--- /dev/null
+++ b/packages/@n8n/instance-ai/src/tools/nodes/__tests__/setup-preference.test.ts
@@ -0,0 +1,57 @@
+import credentialSetupability from '../credential-setupability.json';
+import { addSetupPreference } from '../setup-preference';
+
+function expectedPreference(metric: (typeof credentialSetupability)[number]) {
+ return {
+ type: metric.id,
+ setupCompletionPercent:
+ metric.setupability === null ? null : Math.round(metric.setupability * 100),
+ popularityScore: metric.popularity === null ? null : Math.round(metric.popularity * 10) / 10,
+ };
+}
+
+describe('credential setupability data', () => {
+ it('should contain canonical credential metrics', () => {
+ expect(credentialSetupability.length).toBeGreaterThan(0);
+ expect(new Set(credentialSetupability.map(({ id }) => id)).size).toBe(
+ credentialSetupability.length,
+ );
+
+ for (const metric of credentialSetupability) {
+ expect(Object.keys(metric).sort()).toEqual(['id', 'popularity', 'setupability']);
+ expect(metric.id.trim()).toBe(metric.id);
+ expect(metric.id.length).toBeGreaterThan(0);
+ if (metric.setupability !== null) {
+ expect(metric.setupability).toBeGreaterThanOrEqual(0);
+ expect(metric.setupability).toBeLessThanOrEqual(1);
+ expect(metric.setupability * 20).toBeCloseTo(Math.round(metric.setupability * 20));
+ }
+ if (metric.popularity !== null) {
+ expect(metric.popularity).toBeGreaterThanOrEqual(0);
+ expect(metric.popularity).toBeLessThanOrEqual(1);
+ expect(metric.popularity * 10).toBeCloseTo(Math.round(metric.popularity * 10));
+ }
+ }
+ });
+
+ it('should attach metrics only for credentials supported by the node', () => {
+ const knownMetric = credentialSetupability.find(({ setupability }) => setupability !== null);
+ const unknownMetric = credentialSetupability.find(({ setupability }) => setupability === null);
+ if (!knownMetric || !unknownMetric) throw new Error('Expected known and unknown setupability');
+ const credentialTypes = [knownMetric.id, unknownMetric.id, knownMetric.id, 'missing'];
+ const node = { name: 'n8n-nodes-base.gmail' };
+
+ expect(addSetupPreference(node, credentialTypes)).toEqual({
+ ...node,
+ setupPreference: [expectedPreference(knownMetric), expectedPreference(unknownMetric)].sort(
+ (left, right) => left.type.localeCompare(right.type),
+ ),
+ });
+ });
+
+ it('should leave nodes without matching credential metrics unchanged', () => {
+ const node = { name: 'n8n-nodes-base.unknownSetupability' };
+
+ expect(addSetupPreference(node, ['unknownCredential'])).toBe(node);
+ });
+});
diff --git a/packages/@n8n/instance-ai/src/tools/nodes/credential-setupability.json b/packages/@n8n/instance-ai/src/tools/nodes/credential-setupability.json
new file mode 100644
index 00000000000..4f2203c0955
--- /dev/null
+++ b/packages/@n8n/instance-ai/src/tools/nodes/credential-setupability.json
@@ -0,0 +1,1792 @@
+[
+ {
+ "id": "actionNetworkApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "activeCampaignApi",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "acuitySchedulingApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "acuitySchedulingOAuth2Api",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "adaloApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "affinityApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "agileCrmApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "airtableApi",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "airtableOAuth2Api",
+ "setupability": 0.45,
+ "popularity": 0.7
+ },
+ {
+ "id": "airtableTokenApi",
+ "setupability": 0.35,
+ "popularity": 0.7
+ },
+ {
+ "id": "airtopApi",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "alibabaCloudApi",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "amqp",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "anthropicApi",
+ "setupability": 0.3,
+ "popularity": 0.8
+ },
+ {
+ "id": "apiTemplateIoApi",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "asanaApi",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "asanaOAuth2Api",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "autopilotApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "aws",
+ "setupability": null,
+ "popularity": 0.5
+ },
+ {
+ "id": "awsAssumeRole",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "azureAiSearchApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "azureEntraCognitiveServicesOAuth2Api",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "azureOpenAiApi",
+ "setupability": null,
+ "popularity": 0.5
+ },
+ {
+ "id": "azureStorageOAuth2Api",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "azureStorageSharedKeyApi",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "bambooHrApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "bannerbearApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "baserowApi",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "baserowTokenApi",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "bitbucketAccessTokenApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "bitbucketApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "bitlyApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "bitlyOAuth2Api",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "bitwardenApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "boxOAuth2Api",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "brandfetchApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "bubbleApi",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "calApi",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "calendlyApi",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "calendlyOAuth2Api",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "chromaCloudApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "chromaSelfHostedApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "circleCiApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "ciscoWebexOAuth2Api",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "clearbitApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "clickUpApi",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "clickUpOAuth2Api",
+ "setupability": null,
+ "popularity": 0.5
+ },
+ {
+ "id": "clockifyApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "cloudflareApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "codaApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "cohereApi",
+ "setupability": null,
+ "popularity": 0.5
+ },
+ {
+ "id": "contentfulApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "convertKitApi",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "copperApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "cortexApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "crypto",
+ "setupability": null,
+ "popularity": 0.5
+ },
+ {
+ "id": "customerIoApi",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "databricksApi",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "databricksOAuth2Api",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "deepLApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "deepSeekApi",
+ "setupability": 0.55,
+ "popularity": 0.6
+ },
+ {
+ "id": "demioApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "dhlApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "discordBotApi",
+ "setupability": 0.3,
+ "popularity": 0.5
+ },
+ {
+ "id": "discordOAuth2Api",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "discordWebhookApi",
+ "setupability": null,
+ "popularity": 0.6
+ },
+ {
+ "id": "dropboxApi",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "dropboxOAuth2Api",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "dropcontactApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "elasticsearchApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "emeliaApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "erpNextApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "eventbriteApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "eventbriteOAuth2Api",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "facebookGraphApi",
+ "setupability": 0.15,
+ "popularity": 0.6
+ },
+ {
+ "id": "facebookGraphApiOAuth2Api",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "facebookGraphAppApi",
+ "setupability": 0.25,
+ "popularity": 0.4
+ },
+ {
+ "id": "facebookGraphAppOAuth2Api",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "facebookLeadAdsOAuth2Api",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "figmaApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "figmaOAuth2Api",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "fileMaker",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "flowApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "formIoApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "formstackApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "freshdeskApi",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "freshserviceApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "freshworksCrmApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "ftp",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "gSuiteAdminOAuth2Api",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "getResponseApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "ghostAdminApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "ghostContentApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "gitPassword",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "githubApi",
+ "setupability": 0.4,
+ "popularity": 0.5
+ },
+ {
+ "id": "githubAppApi",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "githubOAuth2Api",
+ "setupability": null,
+ "popularity": 0.5
+ },
+ {
+ "id": "gitlabApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "gitlabOAuth2Api",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "gmailOAuth2",
+ "setupability": 0.6,
+ "popularity": 1
+ },
+ {
+ "id": "goToWebinarOAuth2Api",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "gongApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "gongOAuth2Api",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "googleAdsOAuth2Api",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "googleAnalyticsOAuth2",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "googleApi",
+ "setupability": 0.25,
+ "popularity": 0.7
+ },
+ {
+ "id": "googleBigQueryOAuth2Api",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "googleBooksOAuth2Api",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "googleBusinessProfileOAuth2Api",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "googleCalendarOAuth2Api",
+ "setupability": 0.55,
+ "popularity": 0.9
+ },
+ {
+ "id": "googleChatOAuth2Api",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "googleCloudNaturalLanguageOAuth2Api",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "googleCloudStorageOAuth2Api",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "googleContactsOAuth2Api",
+ "setupability": null,
+ "popularity": 0.5
+ },
+ {
+ "id": "googleDocsOAuth2Api",
+ "setupability": 0.65,
+ "popularity": 0.7
+ },
+ {
+ "id": "googleDriveOAuth2Api",
+ "setupability": 0.65,
+ "popularity": 0.8
+ },
+ {
+ "id": "googleFirebaseCloudFirestoreOAuth2Api",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "googleFirebaseRealtimeDatabaseOAuth2Api",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "googlePalmApi",
+ "setupability": 0.5,
+ "popularity": 0.9
+ },
+ {
+ "id": "googlePerspectiveOAuth2Api",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "googleSheetsOAuth2Api",
+ "setupability": 0.5,
+ "popularity": 1
+ },
+ {
+ "id": "googleSheetsTriggerOAuth2Api",
+ "setupability": 0.55,
+ "popularity": 0.8
+ },
+ {
+ "id": "googleSlidesOAuth2Api",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "googleTasksOAuth2Api",
+ "setupability": null,
+ "popularity": 0.5
+ },
+ {
+ "id": "googleTranslateOAuth2Api",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "gotifyApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "grafanaApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "gristApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "groqApi",
+ "setupability": 0.65,
+ "popularity": 0.8
+ },
+ {
+ "id": "gumroadApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "gumroadOAuth2Api",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "haloPSAApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "harvestApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "harvestOAuth2Api",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "helpScoutOAuth2Api",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "highLevelApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "highLevelOAuth2Api",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "homeAssistantApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "httpBasicAuth",
+ "setupability": 0.4,
+ "popularity": 0.7
+ },
+ {
+ "id": "httpBearerAuth",
+ "setupability": 0.35,
+ "popularity": 0.6
+ },
+ {
+ "id": "httpCustomAuth",
+ "setupability": 0.3,
+ "popularity": 0.1
+ },
+ {
+ "id": "httpHeaderAuth",
+ "setupability": 0.4,
+ "popularity": 0.8
+ },
+ {
+ "id": "httpMultipleHeadersAuth",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "httpQueryAuth",
+ "setupability": 0.35,
+ "popularity": 0.1
+ },
+ {
+ "id": "hubspotApi",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "hubspotAppToken",
+ "setupability": 0.3,
+ "popularity": 0.4
+ },
+ {
+ "id": "hubspotDeveloperApi",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "hubspotOAuth2Api",
+ "setupability": null,
+ "popularity": 0.6
+ },
+ {
+ "id": "huggingFaceApi",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "humanticAiApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "hunterApi",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "imap",
+ "setupability": 0.4,
+ "popularity": 0.6
+ },
+ {
+ "id": "intercomApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "invoiceNinjaApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "iterableApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "jenkinsApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "jinaAiApi",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "jiraSoftwareCloudApi",
+ "setupability": 0.25,
+ "popularity": 0.5
+ },
+ {
+ "id": "jiraSoftwareCloudOAuth2Api",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "jiraSoftwareServerApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "jiraSoftwareServerPatApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "jotFormApi",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "jwtAuth",
+ "setupability": null,
+ "popularity": 0.5
+ },
+ {
+ "id": "kafka",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "keapOAuth2Api",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "koBoToolboxApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "ldap",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "lemlistApi",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "lemonadeApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "lineNotifyOAuth2Api",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "linearApi",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "linearOAuth2Api",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "lingvaNexApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "linkedInCommunityManagementOAuth2Api",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "linkedInOAuth2Api",
+ "setupability": 0.45,
+ "popularity": 0.6
+ },
+ {
+ "id": "magento2Api",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "mailcheckApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "mailchimpApi",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "mailchimpOAuth2Api",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "mailerLiteApi",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "mailgunApi",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "mailjetEmailApi",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "mandrillApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "marketstackApi",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "matrixApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "mattermostApi",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "mauticApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "mauticOAuth2Api",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "mcpOAuth2Api",
+ "setupability": null,
+ "popularity": 0.5
+ },
+ {
+ "id": "mediumApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "messageBirdApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "metabaseApi",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "microsoftAgent365Api",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "microsoftAzureCosmosDbSharedKeyApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "microsoftDynamicsOAuth2Api",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "microsoftEntraOAuth2Api",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "microsoftEntraServicePrincipalApi",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "microsoftExcelOAuth2Api",
+ "setupability": 0.15,
+ "popularity": 0.5
+ },
+ {
+ "id": "microsoftGraphSecurityOAuth2Api",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "microsoftOAuth2Api",
+ "setupability": 0.25,
+ "popularity": 0.6
+ },
+ {
+ "id": "microsoftOneDriveOAuth2Api",
+ "setupability": 0.35,
+ "popularity": 0.5
+ },
+ {
+ "id": "microsoftOutlookOAuth2Api",
+ "setupability": 0.4,
+ "popularity": 0.7
+ },
+ {
+ "id": "microsoftSharePointOAuth2Api",
+ "setupability": 0.25,
+ "popularity": 0.4
+ },
+ {
+ "id": "microsoftSql",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "microsoftTeamsOAuth2Api",
+ "setupability": 0.2,
+ "popularity": 0.5
+ },
+ {
+ "id": "microsoftToDoOAuth2Api",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "milvusApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "mindeeInvoiceApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "mindeeReceiptApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "minimaxApi",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "mispApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "mistralCloudApi",
+ "setupability": null,
+ "popularity": 0.5
+ },
+ {
+ "id": "mondayComApi",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "mondayComOAuth2Api",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "mongoDb",
+ "setupability": null,
+ "popularity": 0.5
+ },
+ {
+ "id": "moonshotApi",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "motorheadApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "mqtt",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "msg91Api",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "mySql",
+ "setupability": null,
+ "popularity": 0.5
+ },
+ {
+ "id": "n8nApi",
+ "setupability": null,
+ "popularity": 0.5
+ },
+ {
+ "id": "nasaApi",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "netlifyApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "nextCloudApi",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "nextCloudOAuth2Api",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "nocoDb",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "nocoDbApiToken",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "notionApi",
+ "setupability": 0.3,
+ "popularity": 0.7
+ },
+ {
+ "id": "notionOAuth2Api",
+ "setupability": null,
+ "popularity": 0.6
+ },
+ {
+ "id": "npmApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "nvidiaApi",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "oAuth2Api",
+ "setupability": 0.2,
+ "popularity": 0.1
+ },
+ {
+ "id": "odooApi",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "odooApiKeyApi",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "oktaApi",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "ollamaApi",
+ "setupability": 0.05,
+ "popularity": 0.4
+ },
+ {
+ "id": "onfleetApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "openAiApi",
+ "setupability": 0.6,
+ "popularity": 0.9
+ },
+ {
+ "id": "openRouterApi",
+ "setupability": 0.55,
+ "popularity": 0.8
+ },
+ {
+ "id": "openWeatherMapApi",
+ "setupability": 0.15,
+ "popularity": 0.7
+ },
+ {
+ "id": "oracleDBApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "ouraApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "paddleApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "pagerDutyApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "pagerDutyOAuth2Api",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "payPalApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "perplexityApi",
+ "setupability": 0.15,
+ "popularity": 0.4
+ },
+ {
+ "id": "phantombusterApi",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "philipsHueOAuth2Api",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "pineconeApi",
+ "setupability": null,
+ "popularity": 0.6
+ },
+ {
+ "id": "pipedriveApi",
+ "setupability": null,
+ "popularity": 0.5
+ },
+ {
+ "id": "pipedriveOAuth2Api",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "plivoApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "postHogApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "postgres",
+ "setupability": 0.15,
+ "popularity": 0.7
+ },
+ {
+ "id": "pushbulletOAuth2Api",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "pushcutApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "pushoverApi",
+ "setupability": null,
+ "popularity": 0.5
+ },
+ {
+ "id": "qdrantApi",
+ "setupability": null,
+ "popularity": 0.5
+ },
+ {
+ "id": "quickBooksOAuth2Api",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "quickbaseApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "rabbitmq",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "raindropOAuth2Api",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "redditOAuth2Api",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "redis",
+ "setupability": null,
+ "popularity": 0.5
+ },
+ {
+ "id": "rocketchatApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "s3",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "salesforceJwtApi",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "salesforceOAuth2Api",
+ "setupability": null,
+ "popularity": 0.5
+ },
+ {
+ "id": "salesmateApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "seaTableApi",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "searXngApi",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "securityScorecardApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "sendGridApi",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "sendInBlueApi",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "sendyApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "sentryIoApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "sentryIoOAuth2Api",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "sentryIoServerApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "serpApi",
+ "setupability": 0.35,
+ "popularity": 0.4
+ },
+ {
+ "id": "serviceNowBasicApi",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "serviceNowOAuth2Api",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "sftp",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "shopifyAccessTokenApi",
+ "setupability": 0.1,
+ "popularity": 0.4
+ },
+ {
+ "id": "shopifyApi",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "shopifyOAuth2Api",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "slackApi",
+ "setupability": 0.2,
+ "popularity": 0.8
+ },
+ {
+ "id": "slackOAuth2Api",
+ "setupability": 0.4,
+ "popularity": 0.7
+ },
+ {
+ "id": "sms77Api",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "smtp",
+ "setupability": 0.2,
+ "popularity": 0.7
+ },
+ {
+ "id": "snowflake",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "snowflakeOAuth2Api",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "splunkApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "spotifyOAuth2Api",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "sshPassword",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "sshPrivateKey",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "stackbyApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "storyblokContentApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "storyblokManagementApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "strapiApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "strapiTokenApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "stravaOAuth2Api",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "stripeApi",
+ "setupability": null,
+ "popularity": 0.5
+ },
+ {
+ "id": "supabaseApi",
+ "setupability": 0.4,
+ "popularity": 0.8
+ },
+ {
+ "id": "surveyMonkeyApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "surveyMonkeyOAuth2Api",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "syncroMspApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "taigaApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "tapfiliateApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "telegramApi",
+ "setupability": 0.55,
+ "popularity": 1
+ },
+ {
+ "id": "theHiveApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "theHiveProjectApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "todoistApi",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "todoistOAuth2Api",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "togglApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "totpApi",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "trelloApi",
+ "setupability": null,
+ "popularity": 0.5
+ },
+ {
+ "id": "trelloOAuth1Api",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "twilioApi",
+ "setupability": 0.25,
+ "popularity": 0.6
+ },
+ {
+ "id": "twistOAuth2Api",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "twitterOAuth1Api",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "twitterOAuth2Api",
+ "setupability": 0.25,
+ "popularity": 0.5
+ },
+ {
+ "id": "typeformApi",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "typeformOAuth2Api",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "unleashedSoftwareApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "upleadApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "uprocApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "uptimeRobotApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "urlScanIoApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "venafiTlsProtectCloudApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "venafiTlsProtectDatacenterApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "vercelAiGatewayApi",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "vonageApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "weaviateApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "webflowOAuth2Api",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "wekanApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "whatsAppApi",
+ "setupability": 0.15,
+ "popularity": 0.8
+ },
+ {
+ "id": "whatsAppTriggerApi",
+ "setupability": 0.15,
+ "popularity": 0.7
+ },
+ {
+ "id": "wiseApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "wolframAlphaApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "wooCommerceApi",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "wordpressApi",
+ "setupability": 0.35,
+ "popularity": 0.5
+ },
+ {
+ "id": "wordpressOAuth2Api",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "workableApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "xAiApi",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "xataApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "xeroOAuth2Api",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "youTubeOAuth2Api",
+ "setupability": 0.45,
+ "popularity": 0.6
+ },
+ {
+ "id": "yourlsApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "zammadBasicAuthApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "zammadTokenAuthApi",
+ "setupability": null,
+ "popularity": 0.1
+ },
+ {
+ "id": "zendeskApi",
+ "setupability": null,
+ "popularity": 0.4
+ },
+ {
+ "id": "zendeskOAuth2Api",
+ "setupability": null,
+ "popularity": 0.3
+ },
+ {
+ "id": "zepApi",
+ "setupability": null,
+ "popularity": 0
+ },
+ {
+ "id": "zohoOAuth2Api",
+ "setupability": null,
+ "popularity": 0.5
+ },
+ {
+ "id": "zoomApi",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "zoomOAuth2Api",
+ "setupability": null,
+ "popularity": 0.2
+ },
+ {
+ "id": "zulipApi",
+ "setupability": null,
+ "popularity": 0
+ }
+]
diff --git a/packages/@n8n/instance-ai/src/tools/nodes/setup-preference.ts b/packages/@n8n/instance-ai/src/tools/nodes/setup-preference.ts
new file mode 100644
index 00000000000..e1cbd895e55
--- /dev/null
+++ b/packages/@n8n/instance-ai/src/tools/nodes/setup-preference.ts
@@ -0,0 +1,33 @@
+import credentialSetupability from './credential-setupability.json';
+
+export interface SetupPreference {
+ type: string;
+ setupCompletionPercent: number | null;
+ popularityScore: number | null;
+}
+
+export type NodeWithSetupPreference = T & {
+ setupPreference?: SetupPreference[];
+};
+
+const setupPreferences = new Map();
+
+for (const { id, setupability, popularity } of credentialSetupability) {
+ setupPreferences.set(id, {
+ type: id,
+ setupCompletionPercent: setupability === null ? null : Math.round(setupability * 100),
+ popularityScore: popularity === null ? null : Math.round(popularity * 10) / 10,
+ });
+}
+
+export function addSetupPreference(
+ node: T,
+ credentialTypes: readonly string[],
+): NodeWithSetupPreference {
+ const credentials = [...new Set(credentialTypes)]
+ .map((credentialType) => setupPreferences.get(credentialType))
+ .filter((preference): preference is SetupPreference => preference !== undefined)
+ .sort((left, right) => left.type.localeCompare(right.type));
+
+ return credentials.length > 0 ? { ...node, setupPreference: credentials } : node;
+}
diff --git a/packages/@n8n/instance-ai/tsconfig.build.json b/packages/@n8n/instance-ai/tsconfig.build.json
index 061bbe7166d..25fffe6f06e 100644
--- a/packages/@n8n/instance-ai/tsconfig.build.json
+++ b/packages/@n8n/instance-ai/tsconfig.build.json
@@ -5,6 +5,6 @@
"rootDir": "src",
"outDir": "dist"
},
- "include": ["src/**/*.ts"],
+ "include": ["src/**/*.ts", "src/**/*.json"],
"exclude": ["src/**/__tests__/**", "src/**/test-utils/**", "**/*.test.ts"]
}